АДРЕСНЫЙ РЕЖИМ - M2.3c тюнинг резолвинга и фильтров адресных запросов, поэтапная диагностика и аудит фильтрации по счету

This commit is contained in:
2026-03-29 21:51:09 +03:00
parent 2bf16de4ea
commit a2886faed6
37 changed files with 3050 additions and 151 deletions
@@ -7,6 +7,7 @@ import type {
AddressFilterSet,
AddressIntent,
AddressLimitedReasonCategory,
AddressMatchFailureStage,
AddressMcpCallStatus,
AddressQueryShapeDetection,
AddressResponseType,
@@ -28,6 +29,30 @@ interface NormalizedAddressRow {
analytics: string[];
}
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 PARTY_ANCHOR_STOPWORDS = new Set([
"ооо",
"ао",
"зао",
"ип",
"llc",
"ltd",
"company",
"компания",
"контрагент",
"counterparty",
"по",
"by"
]);
const ACCOUNT_ALIAS_MAP: Record<string, string[]> = {
"51": ["расчетный счет", "расчетные счета", "bank account"],
"52": ["валютный счет", "валютные счета", "currency account"],
"60": ["поставщик", "поставщиками", "подрядчиками", "расчеты с поставщиками"],
"62": ["покупатель", "покупателями", "расчеты с покупателями"],
"76": ["прочие расчеты", "прочими дебиторами и кредиторами"]
};
function parseFiniteNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
@@ -48,12 +73,124 @@ function valueAsString(value: unknown): string {
return String(value);
}
function normalizeToken(value: string): string {
return String(value ?? "").trim().toLowerCase();
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 escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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 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 tokenLatin = transliterateCyrillicToLatin(token);
return searchableNormalized.includes(token) || searchableLatin.includes(tokenLatin);
});
}
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[] {
@@ -147,13 +284,27 @@ function rowMatchesAnyAccount(row: NormalizedAddressRow, accountScope: string[])
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 normalized = String(account ?? "").trim();
if (!normalized) {
const normalizedRequested = normalizeAccountToken(String(account ?? "").trim());
if (!normalizedRequested) {
return false;
}
const matcher = new RegExp(`\\b${escapeRegExp(normalized)}(?:\\.\\d{1,2})?\\b`, "i");
return matcher.test(searchable);
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);
});
});
}
@@ -164,30 +315,55 @@ function applyAccountScopeFilter(rows: NormalizedAddressRow[], accountScope: str
return rows.filter((row) => rowMatchesAnyAccount(row, accountScope));
}
function applyAddressFilters(rows: NormalizedAddressRow[], filters: AddressFilterSet): NormalizedAddressRow[] {
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 = normalizeToken(String(filters.counterparty));
filtered = filtered.filter((row) => rowSearchableText(row).includes(needle));
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 = normalizeToken(String(filters.contract));
filtered = filtered.filter((row) => rowSearchableText(row).includes(needle));
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.document_ref && String(filters.document_ref).trim()) {
const needle = normalizeToken(String(filters.document_ref));
filtered = filtered.filter((row) => rowSearchableText(row).includes(needle));
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 filtered;
return {
rows: filtered,
mismatchReason
};
}
function applyIntentSpecificFilter(intent: AddressIntent, rows: NormalizedAddressRow[]): NormalizedAddressRow[] {
@@ -261,6 +437,18 @@ interface RowStageDiagnostics {
| "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);
}
@@ -303,11 +491,63 @@ function deriveRowStageDiagnostics(
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) {
@@ -322,12 +562,24 @@ function deriveMcpStageStatus(input: {
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_not_matched";
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 resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet): AnchorResolutionDebug {
const account = typeof filters.account === "string" ? filters.account.trim() : "";
const counterparty = typeof filters.counterparty === "string" ? filters.counterparty.trim() : "";
@@ -385,6 +637,42 @@ function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilterSet):
};
}
function refineAnchorFromRows(anchor: AnchorResolutionDebug, rows: NormalizedAddressRow[]): AnchorResolutionDebug {
if (rows.length === 0) {
return anchor;
}
if (anchor.anchor_type !== "counterparty" && anchor.anchor_type !== "contract") {
return anchor;
}
const needleRaw = String(anchor.anchor_value_raw ?? "").trim();
if (!needleRaw) {
return anchor;
}
const candidates = uniqueStrings(
rows
.flatMap((row) => row.analytics)
.map((value) => value.trim())
.filter((value) => value.length >= 2 && matchesAnchorText(value, needleRaw))
);
if (candidates.length === 0) {
return anchor;
}
if (candidates.length === 1) {
return {
...anchor,
anchor_value_resolved: candidates[0],
resolver_confidence: anchor.resolver_confidence === "high" ? "high" : "medium",
ambiguity_count: 0
};
}
return {
...anchor,
anchor_value_resolved: candidates[0],
resolver_confidence: "low",
ambiguity_count: candidates.length - 1
};
}
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
const heading =
category === "empty_match"
@@ -415,7 +703,10 @@ function buildLimitedExecutionResult(input: {
selectedRecipe: string | null;
accountScopeMode?: "strict" | "preferred";
accountScopeFallbackApplied?: boolean;
accountScopeAudit?: AccountScopeAuditDebug;
anchor?: AnchorResolutionDebug;
matchFailureStage?: AddressMatchFailureStage;
matchFailureReason?: string | null;
mcpCallStatus: AddressMcpCallStatus;
rowsFetched: number;
rawRowsReceived?: number;
@@ -437,6 +728,7 @@ function buildLimitedExecutionResult(input: {
nextStep?: string;
category: AddressLimitedReasonCategory;
}): AddressExecutionResult {
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
return {
handled: true,
reply_text: composeLimitedReply(input.category, input.reasonText, input.nextStep),
@@ -452,6 +744,7 @@ function buildLimitedExecutionResult(input: {
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,
@@ -459,6 +752,8 @@ function buildLimitedExecutionResult(input: {
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,
@@ -468,6 +763,11 @@ function buildLimitedExecutionResult(input: {
rows_matched: input.rowsMatched,
raw_row_keys_sample: input.rawRowKeysSample ?? [],
materialization_drop_reason: input.materializationDropReason ?? "none",
account_token_raw: accountScopeAudit.accountTokenRaw,
account_token_normalized: accountScopeAudit.accountTokenNormalized,
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
limited_reason_category: input.category,
response_type: "LIMITED_WITH_REASON",
@@ -605,7 +905,7 @@ export class AddressQueryService {
const intent = resolveAddressIntent(userMessage);
const filters = extractAddressFilters(userMessage, intent.intent);
const anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
const recipeSelection = selectAddressRecipe(intent.intent, filters.extracted_filters);
const baseReasons = [...mode.reasons, ...shape.reasons, ...intent.reasons];
@@ -720,6 +1020,7 @@ export class AddressQueryService {
});
if (mcp.error) {
const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters);
return buildLimitedExecutionResult({
mode,
shape,
@@ -733,8 +1034,10 @@ export class AddressQueryService {
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,
@@ -759,16 +1062,44 @@ export class AddressQueryService {
normalizedRawRows.length > 0 &&
scopedRows.length === 0;
const normalizedRows = accountScopeFallbackApplied ? normalizedRawRows : scopedRows;
const filterByAnchors = applyAddressFilters(normalizedRows, filters.extracted_filters);
anchor = refineAnchorFromRows(anchor, normalizedRows);
const filtersForMatching: AddressFilterSet =
anchor.anchor_type === "counterparty" && anchor.anchor_value_resolved
? { ...filters.extracted_filters, counterparty: anchor.anchor_value_resolved }
: anchor.anchor_type === "contract" && anchor.anchor_value_resolved
? { ...filters.extracted_filters, contract: anchor.anchor_value_resolved }
: filters.extracted_filters;
const accountScopeAudit = buildAccountScopeAudit({
intent: intent.intent,
filters: filtersForMatching,
accountScope: plan.account_scope,
rowsBeforeScope: normalizedRawRows.length,
rowsAfterScope: normalizedRows.length
});
const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
const filterByAnchors = anchorFilter.rows;
const filteredRows = applyIntentSpecificFilter(intent.intent, filterByAnchors);
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 (intent.intent === "list_open_contracts" && contractCandidatesFromRows(filteredRows).length === 0) {
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) {
return buildLimitedExecutionResult({
mode,
shape,
@@ -778,7 +1109,10 @@ export class AddressQueryService {
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
accountScopeMode: plan.account_scope_mode,
accountScopeFallbackApplied,
accountScopeAudit,
anchor,
matchFailureStage,
matchFailureReason,
mcpCallStatus: stageStatus,
rowsFetched: mcp.fetched_rows,
rawRowsReceived: mcp.raw_rows.length,
@@ -803,6 +1137,38 @@ export class AddressQueryService {
hadBaseRows &&
hadAnchorMatchedRows &&
(intent.intent === "list_documents_by_counterparty" || intent.intent === "bank_operations_by_counterparty");
const isAnchorMismatch = stageStatus === "materialized_but_not_anchor_matched";
const isRecipeFilteredOut = stageStatus === "materialized_but_filtered_out_by_recipe";
const category: AddressLimitedReasonCategory = isAnchorMismatch
? "missing_anchor"
: isRecipeFilteredOut
? "recipe_visibility_gap"
: isVisibilityGapCandidate
? "recipe_visibility_gap"
: "empty_match";
const reasonText = isAnchorMismatch
? "якорь контрагента/договора не найден в материализованных live-строках"
: isRecipeFilteredOut
? "строки по якорю найдены, но отфильтрованы intent-specific recipe"
: isVisibilityGapCandidate
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
: "по выбранным фильтрам в live-выборке нет строк";
const nextStep = isAnchorMismatch
? "уточните контрагента точным именем или добавьте ИНН/договор"
: isRecipeFilteredOut
? "сузьте период, уточните контрагента или документный тип"
: isVisibilityGapCandidate
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
: "уточните период, контрагента, договор или снимите часть фильтров";
const limitations = isAnchorMismatch
? ["anchor_not_matched_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,
@@ -812,7 +1178,10 @@ export class AddressQueryService {
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
accountScopeMode: plan.account_scope_mode,
accountScopeFallbackApplied,
accountScopeAudit,
anchor,
matchFailureStage,
matchFailureReason,
mcpCallStatus: stageStatus,
rowsFetched: mcp.fetched_rows,
rawRowsReceived: mcp.raw_rows.length,
@@ -822,18 +1191,10 @@ export class AddressQueryService {
rowsMatched: 0,
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
materializationDropReason: rowDiagnostics.materializationDropReason,
category: isVisibilityGapCandidate ? "recipe_visibility_gap" : "empty_match",
reasonText: isVisibilityGapCandidate
? "в текущем live recipe нет достаточной document/bank видимости после фильтрации"
: "по выбранным фильтрам в live-выборке нет строк",
nextStep: isVisibilityGapCandidate
? "нужен специализированный recipe для document/bank контуров или более точный документный anchor"
: "уточните период, контрагента, договор или снимите часть фильтров",
limitations: [
isVisibilityGapCandidate
? "document_or_bank_visibility_gap_after_base_filter"
: "no_rows_after_recipe_and_scope_filter"
],
category,
reasonText,
nextStep,
limitations,
reasons: baseReasons
});
}
@@ -854,6 +1215,7 @@ export class AddressQueryService {
extracted_filters: filters.extracted_filters,
missing_required_filters: [],
selected_recipe: recipeSelection.selected_recipe.recipe_id,
mcp_call_status_legacy: toLegacyMcpStatus(stageStatus),
account_scope_mode: plan.account_scope_mode,
account_scope_fallback_applied: accountScopeFallbackApplied,
anchor_type: anchor.anchor_type,
@@ -861,6 +1223,8 @@ export class AddressQueryService {
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,
@@ -870,6 +1234,11 @@ export class AddressQueryService {
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,