@@ -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 ,