Этап 4 corrective pack 2 по family isolation после текущих routing fixes
This commit is contained in:
@@ -8,7 +8,8 @@ export type ClaimType =
|
||||
| "prove_advance_offset_state"
|
||||
| "prove_vat_chain_completeness"
|
||||
| "prove_month_close_state"
|
||||
| "prove_rbp_tail_state";
|
||||
| "prove_rbp_tail_state"
|
||||
| "prove_fixed_asset_amortization_coverage";
|
||||
|
||||
export type ContextExpansionReason =
|
||||
| "prehistory"
|
||||
@@ -24,6 +25,7 @@ export interface TemporalWindow {
|
||||
|
||||
export interface ClaimBoundAnchorAudit {
|
||||
claim_type: ClaimType;
|
||||
settlement_role?: "supplier" | "customer" | "mixed" | "unknown";
|
||||
required_anchors: string[];
|
||||
resolved_anchors: Record<string, string[]>;
|
||||
missing_anchors: string[];
|
||||
@@ -42,6 +44,18 @@ export interface TargetedEvidenceAcquisitionAudit {
|
||||
targeted_evidence_hits: number;
|
||||
targeted_evidence_hit_rate: number;
|
||||
targeted_evidence_source_refs: string[];
|
||||
fa_expected_set?: string[];
|
||||
fa_actual_set_from_amortization?: string[];
|
||||
fa_missing_candidates?: string[];
|
||||
fa_uncertain_candidates?: string[];
|
||||
fa_relation_map?: Array<{
|
||||
fa_object: string;
|
||||
document_amortization: string[];
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
period: string[];
|
||||
coverage_status: "covered" | "missing" | "uncertain";
|
||||
}>;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
@@ -112,35 +126,108 @@ function shiftDays(iso: string, deltaDays: number): string | null {
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
function inferClaimType(input: { userMessage: string; focusDomainHint?: string | null }): ClaimType {
|
||||
function accountPrefix(value: string): string | null {
|
||||
const token = String(value ?? "").trim();
|
||||
const match = token.match(/^(\d{2})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function accountPrefixesFromAnchors(anchors?: CompanyAnchorSet | null): Set<string> {
|
||||
const prefixes = new Set<string>();
|
||||
const accounts = Array.isArray(anchors?.accounts) ? anchors.accounts : [];
|
||||
for (const item of accounts) {
|
||||
const prefix = accountPrefix(String(item ?? ""));
|
||||
if (prefix) {
|
||||
prefixes.add(prefix);
|
||||
}
|
||||
}
|
||||
return prefixes;
|
||||
}
|
||||
|
||||
function inferClaimType(input: { userMessage: string; focusDomainHint?: string | null; companyAnchors?: CompanyAnchorSet | null }): ClaimType {
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const isVat =
|
||||
input.focusDomainHint === "vat_document_register_book" ||
|
||||
/(?:\bvat\b|ндс|invoice|счет[- ]фактур|register|книга покупок|книга продаж)/i.test(lower);
|
||||
if (isVat) {
|
||||
const accountPrefixes = accountPrefixesFromAnchors(input.companyAnchors);
|
||||
|
||||
const hasSettlementAccount = ["51", "60", "62", "76"].some((item) => accountPrefixes.has(item));
|
||||
const hasVatAccount = ["19", "68"].some((item) => accountPrefixes.has(item));
|
||||
const hasFixedAssetAccount = ["01", "02", "08"].some((item) => accountPrefixes.has(item));
|
||||
const hasRbpAccount = accountPrefixes.has("97");
|
||||
const hasMonthCloseAccount = ["20", "21", "23", "25", "26", "28", "29", "44"].some((item) =>
|
||||
accountPrefixes.has(item)
|
||||
);
|
||||
|
||||
const hasAdvanceSignal = /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(lower);
|
||||
const hasSettlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|плате[жж]|платёж|постав|покупател|settlement|payment|supplier|customer)/i.test(
|
||||
lower
|
||||
);
|
||||
const hasVatLexical = /(?:\bvat\b|ндс|invoice|сч[её]т[- ]?фактур|register|книга\s+покупок|книга\s+продаж|книг[аи]\s+(?:покуп|продаж))/i.test(
|
||||
lower
|
||||
);
|
||||
const hasFixedAssetLexical = /(?:depreciat|amortization|fixed\s*asset|амортиз|основн(?:ые|ых)?\s+сред|объект\s+ос|сч[её]т\s*0[128]|account\s*0[128])/i.test(
|
||||
lower
|
||||
);
|
||||
const hasRbpLexical = /(?:\brbp\b|рбп|deferred\s*expense|writeoff|расходы\s+будущих\s+периодов|списани[ея]\s+рбп|account\s*97|сч[её]т\s*97)/i.test(
|
||||
lower
|
||||
);
|
||||
const hasMonthCloseLexical = /(?:month[- ]?close|закрыт|закрытие\s+месяца|косвен|account\s*20|account\s*44|сч[её]т\s*20|сч[её]т\s*44|распределен|period\s*close)/i.test(
|
||||
lower
|
||||
);
|
||||
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
return hasAdvanceSignal ? "prove_advance_offset_state" : "prove_settlement_closure_state";
|
||||
}
|
||||
if (input.focusDomainHint === "vat_document_register_book") {
|
||||
return "prove_vat_chain_completeness";
|
||||
}
|
||||
const isRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred expense|writeoff)/i.test(lower);
|
||||
if (isRbp) {
|
||||
return "prove_rbp_tail_state";
|
||||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||||
return "prove_fixed_asset_amortization_coverage";
|
||||
}
|
||||
const isMonthClose =
|
||||
input.focusDomainHint === "month_close_costs_20_44" ||
|
||||
/(?:month[- ]?close|закрыт|косвен|account\s*20|account\s*44|счет\s*20|счет\s*44)/i.test(lower);
|
||||
if (isMonthClose) {
|
||||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
if (hasRbpLexical || hasRbpAccount) {
|
||||
return "prove_rbp_tail_state";
|
||||
}
|
||||
return "prove_month_close_state";
|
||||
}
|
||||
const isAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower);
|
||||
if (isAdvance) {
|
||||
|
||||
const settlementPriority =
|
||||
(hasSettlementLexical || hasSettlementAccount || hasAdvanceSignal) && !hasVatLexical && !hasFixedAssetLexical;
|
||||
const broadMonthClosePriority =
|
||||
(hasMonthCloseLexical || hasMonthCloseAccount) &&
|
||||
!hasVatLexical &&
|
||||
!hasVatAccount &&
|
||||
!hasFixedAssetLexical &&
|
||||
!hasFixedAssetAccount;
|
||||
|
||||
if (hasAdvanceSignal && settlementPriority) {
|
||||
return "prove_advance_offset_state";
|
||||
}
|
||||
if (settlementPriority) {
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
if (hasVatLexical || (hasVatAccount && !settlementPriority)) {
|
||||
return "prove_vat_chain_completeness";
|
||||
}
|
||||
if (broadMonthClosePriority) {
|
||||
return hasRbpLexical || hasRbpAccount ? "prove_rbp_tail_state" : "prove_month_close_state";
|
||||
}
|
||||
if (hasFixedAssetLexical || (hasFixedAssetAccount && !settlementPriority && !hasVatLexical)) {
|
||||
return "prove_fixed_asset_amortization_coverage";
|
||||
}
|
||||
if (hasRbpLexical || hasRbpAccount) {
|
||||
return "prove_rbp_tail_state";
|
||||
}
|
||||
if (hasMonthCloseLexical || hasMonthCloseAccount) {
|
||||
return "prove_month_close_state";
|
||||
}
|
||||
if (hasSettlementLexical || hasSettlementAccount) {
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
return "prove_settlement_closure_state";
|
||||
}
|
||||
|
||||
function inferCounterpartyScope(message: string): string[] {
|
||||
const lower = message.toLowerCase();
|
||||
const out: string[] = [];
|
||||
if (/(?:supplier|vendor|поставщик)/i.test(lower)) out.push("supplier");
|
||||
if (/(?:supplier|vendor|поставщик|кредитор)/i.test(lower)) out.push("supplier");
|
||||
if (/(?:customer|buyer|покупатель|дебитор)/i.test(lower)) out.push("customer");
|
||||
return uniqueStrings(out);
|
||||
}
|
||||
@@ -148,14 +235,46 @@ function inferCounterpartyScope(message: string): string[] {
|
||||
function detectSignals(message: string): Record<string, boolean> {
|
||||
const lower = message.toLowerCase();
|
||||
return {
|
||||
hasAdvance: /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower),
|
||||
hasClosure: /(?:close|closure|закрыт|хвост|tail|reconcile|зачет)/i.test(lower),
|
||||
hasVat: /(?:\bvat\b|ндс|счет[- ]фактур|invoice|книга покупок|книга продаж|register)/i.test(lower),
|
||||
hasMonthClose: /(?:month[- ]?close|закрытие месяца|косвен|20\/44|account 20|account 44|счет 20|счет 44)/i.test(lower),
|
||||
hasRbp: /(?:\brbp\b|рбп|account 97|счет 97|writeoff|списани)/i.test(lower)
|
||||
hasAdvance: /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(lower),
|
||||
hasClosure: /(?:close|closure|закрыт|хвост|tail|reconcile|зач[её]т)/i.test(lower),
|
||||
hasVat: /(?:\bvat\b|ндс|сч[её]т[- ]?фактур|invoice|книга\s+покупок|книга\s+продаж|register)/i.test(lower),
|
||||
hasMonthClose: /(?:month[- ]?close|закрытие\s+месяца|косвен|20\/44|account 20|account 44|сч[её]т 20|сч[её]т 44)/i.test(lower),
|
||||
hasRbp: /(?:\brbp\b|рбп|account 97|сч[её]т 97|writeoff|списани)/i.test(lower),
|
||||
hasFixedAsset: /(?:depreciat|amortization|fixed\s*asset|амортиз|основн(?:ые|ых)?\s+сред|объект\s+ос|сч[её]т\s*0[128]|account\s*0[128])/i.test(
|
||||
lower
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSettlementRole(input: {
|
||||
claimType: ClaimType;
|
||||
counterpartyScope: string[];
|
||||
accountPrefixes: Set<string>;
|
||||
userMessage: string;
|
||||
}): "supplier" | "customer" | "mixed" | "unknown" | undefined {
|
||||
if (input.claimType !== "prove_settlement_closure_state" && input.claimType !== "prove_advance_offset_state") {
|
||||
return undefined;
|
||||
}
|
||||
const scopes = new Set(input.counterpartyScope.map((item) => String(item ?? "").trim().toLowerCase()));
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const hasSupplierLexical = /(?:supplier|vendor|поставщ|кредитор|обязательств|payable)/i.test(lower);
|
||||
const hasCustomerLexical = /(?:customer|buyer|покупат|дебитор|receivable)/i.test(lower);
|
||||
const hasSupplierAccount = input.accountPrefixes.has("60");
|
||||
const hasCustomerAccount = input.accountPrefixes.has("62");
|
||||
const supplierSignal = scopes.has("supplier") || hasSupplierLexical || (hasSupplierAccount && !hasCustomerAccount);
|
||||
const customerSignal = scopes.has("customer") || hasCustomerLexical || (hasCustomerAccount && !hasSupplierAccount);
|
||||
if (supplierSignal && !customerSignal) {
|
||||
return "supplier";
|
||||
}
|
||||
if (customerSignal && !supplierSignal) {
|
||||
return "customer";
|
||||
}
|
||||
if (supplierSignal && customerSignal) {
|
||||
return "mixed";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function mergeAnchors(anchors: CompanyAnchorSet | null | undefined, key: keyof CompanyAnchorSet): string[] {
|
||||
return uniqueStrings(Array.isArray(anchors?.[key]) ? (anchors?.[key] as string[]) : []);
|
||||
}
|
||||
@@ -191,6 +310,22 @@ function missingFromRequired(required: string[], resolved: Record<string, string
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (anchor === "amount_or_document") {
|
||||
const hasAmount = (resolved.amounts?.length ?? 0) > 0;
|
||||
const hasDoc = (resolved.document_numbers?.length ?? 0) > 0 || (resolved.document_types?.length ?? 0) > 0;
|
||||
if (!hasAmount && !hasDoc) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (anchor === "account_scope_or_document_type") {
|
||||
const hasAccount = (resolved.account_scope?.length ?? 0) > 0;
|
||||
const hasDocType = (resolved.document_types?.length ?? 0) > 0;
|
||||
if (!hasAccount && !hasDocType) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((resolved[anchor]?.length ?? 0) <= 0) {
|
||||
missing.push(anchor);
|
||||
}
|
||||
@@ -206,9 +341,28 @@ export function resolveClaimBoundAnchors(input: {
|
||||
}): ClaimBoundAnchorAudit {
|
||||
const claimType = inferClaimType({
|
||||
userMessage: input.userMessage,
|
||||
focusDomainHint: input.focusDomainHint
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
companyAnchors: input.companyAnchors
|
||||
});
|
||||
const signals = detectSignals(input.userMessage);
|
||||
const accountPrefixes = accountPrefixesFromAnchors(input.companyAnchors);
|
||||
const includeVatAnchors = claimType === "prove_vat_chain_completeness";
|
||||
const includeMonthCloseAnchors = claimType === "prove_month_close_state";
|
||||
const includeRbpAnchors = claimType === "prove_rbp_tail_state";
|
||||
const includeFixedAssetAnchors = claimType === "prove_fixed_asset_amortization_coverage";
|
||||
const hasVatSignal = signals.hasVat || accountPrefixes.has("19") || accountPrefixes.has("68");
|
||||
const hasRbpSignal = signals.hasRbp || accountPrefixes.has("97");
|
||||
const hasFixedAssetSignal = signals.hasFixedAsset || accountPrefixes.has("01") || accountPrefixes.has("02") || accountPrefixes.has("08");
|
||||
const hasMonthCloseSignal =
|
||||
signals.hasMonthClose ||
|
||||
accountPrefixes.has("20") ||
|
||||
accountPrefixes.has("21") ||
|
||||
accountPrefixes.has("23") ||
|
||||
accountPrefixes.has("25") ||
|
||||
accountPrefixes.has("26") ||
|
||||
accountPrefixes.has("28") ||
|
||||
accountPrefixes.has("29") ||
|
||||
accountPrefixes.has("44");
|
||||
const resolvedAnchors: Record<string, string[]> = {
|
||||
period: uniqueStrings([...mergeAnchors(input.companyAnchors, "periods"), ...mergeAnchors(input.companyAnchors, "dates")]),
|
||||
account_scope: mergeAnchors(input.companyAnchors, "accounts"),
|
||||
@@ -219,16 +373,28 @@ export function resolveClaimBoundAnchors(input: {
|
||||
counterparty_scope: inferCounterpartyScope(input.userMessage),
|
||||
advance_signal: signals.hasAdvance ? ["advance"] : [],
|
||||
closure_signal: signals.hasClosure ? ["closure"] : [],
|
||||
vat_signal: signals.hasVat ? ["vat"] : [],
|
||||
chain_signal: signals.hasVat ? ["chain"] : [],
|
||||
close_signal: signals.hasMonthClose ? ["month_close"] : [],
|
||||
vat_signal: includeVatAnchors && hasVatSignal ? ["vat"] : [],
|
||||
chain_signal: includeVatAnchors && hasVatSignal ? ["chain"] : [],
|
||||
close_signal: includeMonthCloseAnchors && hasMonthCloseSignal ? ["month_close"] : [],
|
||||
cost_scope: [],
|
||||
rbp_signal: signals.hasRbp ? ["rbp"] : [],
|
||||
writeoff_signal: signals.hasRbp ? ["writeoff"] : []
|
||||
rbp_signal: includeRbpAnchors && hasRbpSignal ? ["rbp"] : [],
|
||||
writeoff_signal: includeRbpAnchors && hasRbpSignal ? ["writeoff"] : [],
|
||||
fixed_asset_signal: includeFixedAssetAnchors && hasFixedAssetSignal ? ["fixed_asset"] : [],
|
||||
amortization_signal: includeFixedAssetAnchors && hasFixedAssetSignal ? ["amortization"] : [],
|
||||
expected_fa_set: [],
|
||||
actual_fa_set: []
|
||||
};
|
||||
if (/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || signals.hasMonthClose) {
|
||||
if (
|
||||
includeMonthCloseAnchors &&
|
||||
(/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || hasMonthCloseSignal)
|
||||
) {
|
||||
resolvedAnchors.cost_scope = ["20_44"];
|
||||
}
|
||||
// For FA amortization claims, document type is implicit in user intent
|
||||
// even when the phrase does not carry explicit document keywords.
|
||||
if (includeFixedAssetAnchors && hasFixedAssetSignal && (resolvedAnchors.document_types?.length ?? 0) <= 0) {
|
||||
resolvedAnchors.document_types = ["amortization_document"];
|
||||
}
|
||||
if (input.primaryPeriod) {
|
||||
resolvedAnchors.period = uniqueStrings([...(resolvedAnchors.period ?? []), input.primaryPeriod.from, input.primaryPeriod.to]);
|
||||
}
|
||||
@@ -238,7 +404,14 @@ export function resolveClaimBoundAnchors(input: {
|
||||
prove_advance_offset_state: ["period", "account_scope", "advance_signal", "settlement_object"],
|
||||
prove_vat_chain_completeness: ["period", "document_types", "vat_signal", "chain_signal"],
|
||||
prove_month_close_state: ["period", "close_signal", "cost_scope"],
|
||||
prove_rbp_tail_state: ["period", "rbp_signal", "writeoff_signal"]
|
||||
prove_rbp_tail_state: ["period", "rbp_signal", "writeoff_signal"],
|
||||
prove_fixed_asset_amortization_coverage: [
|
||||
"period",
|
||||
"fixed_asset_signal",
|
||||
"amortization_signal",
|
||||
"amount_or_document",
|
||||
"account_scope_or_document_type"
|
||||
]
|
||||
};
|
||||
|
||||
const requiredAnchors = requiredByClaim[claimType];
|
||||
@@ -258,9 +431,22 @@ export function resolveClaimBoundAnchors(input: {
|
||||
if (!allowedContextWindow && input.primaryPeriod) {
|
||||
reasonCodes.push("controlled_temporal_expansion_window_unavailable");
|
||||
}
|
||||
const settlementRole = resolveSettlementRole({
|
||||
claimType,
|
||||
counterpartyScope: resolvedAnchors.counterparty_scope ?? [],
|
||||
accountPrefixes,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
if (
|
||||
(claimType === "prove_settlement_closure_state" || claimType === "prove_advance_offset_state") &&
|
||||
(settlementRole === "mixed" || settlementRole === "unknown")
|
||||
) {
|
||||
reasonCodes.push("unresolved_supplier_customer_polarity");
|
||||
}
|
||||
|
||||
return {
|
||||
claim_type: claimType,
|
||||
settlement_role: settlementRole,
|
||||
required_anchors: requiredAnchors,
|
||||
resolved_anchors: resolvedAnchors,
|
||||
missing_anchors: missingAnchors,
|
||||
@@ -288,7 +474,13 @@ function buildCorpusFromItem(item: Record<string, unknown>): string {
|
||||
document_context: item.document_context,
|
||||
relation_pattern_hits: item.relation_pattern_hits,
|
||||
graph_domain_scope: item.graph_domain_scope,
|
||||
lifecycle_markers: item.lifecycle_markers
|
||||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
fa_object_hint: item.fa_object_hint,
|
||||
fa_expected_set_candidate: item.fa_expected_set_candidate,
|
||||
fa_actual_set_candidate: item.fa_actual_set_candidate,
|
||||
fa_coverage_status: item.fa_coverage_status
|
||||
}).toLowerCase();
|
||||
}
|
||||
|
||||
@@ -329,6 +521,16 @@ function requiredChecksByClaim(claimType: ClaimType): string[] {
|
||||
if (claimType === "prove_month_close_state") {
|
||||
return ["close_operation_found", "distribution_step_found", "residual_tail_found"];
|
||||
}
|
||||
if (claimType === "prove_fixed_asset_amortization_coverage") {
|
||||
return [
|
||||
"amortization_document_found",
|
||||
"fixed_asset_object_identified",
|
||||
"expected_fa_set_reconstructed",
|
||||
"actual_fa_set_reconstructed",
|
||||
"movement_or_posting_link_found",
|
||||
"missing_fa_candidates_assessed"
|
||||
];
|
||||
}
|
||||
return [
|
||||
"rbp_writeoff_document_found",
|
||||
"rbp_object_identified",
|
||||
@@ -348,21 +550,34 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
||||
const hasSettlementAccount = /(?:\b60(?:\.\d{2})?\b|\b62(?:\.\d{2})?\b|payable|receivable|settlement)/i.test(corpus);
|
||||
const hasPosting = /(?:document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasRegister = /(?:register|accumulationregister|accountingregister|регистр)/i.test(corpus);
|
||||
const hasClose = /(?:close|closure|закрыт|reconcile|зачет|tail|хвост)/i.test(corpus);
|
||||
const hasClose = /(?:close|closure|закрыт|reconcile|зач[её]т|tail|хвост)/i.test(corpus);
|
||||
const hasPayment = /(?:payment|оплат|списаниесрасчетногосчета|payment_order|bank_statement)/i.test(corpus);
|
||||
const hasAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(corpus);
|
||||
const hasVat = /(?:\bvat\b|ндс|invoice_to_vat|счет[- ]фактур|invoice)/i.test(corpus);
|
||||
const hasBook = /(?:книгипокупок|книгипродаж|book)/i.test(corpus);
|
||||
const hasAdvance = /(?:advance|аванс|offset|зач[её]т|62\.02|60\.02)/i.test(corpus);
|
||||
const hasVat = /(?:\bvat\b|ндс|invoice_to_vat|сч[её]т[- ]?фактур|invoice)/i.test(corpus);
|
||||
const hasBook = /(?:книг[аи](?:\s+)?(?:покупок|продаж)|book)/i.test(corpus);
|
||||
const hasChain = /(?:chain|link|document_to_posting|invoice_to_vat|связ)/i.test(corpus);
|
||||
const hasMonthClose = /(?:month[- ]?close|period_close|закрытие месяца|косвен|20|44)/i.test(corpus);
|
||||
const hasMonthClose = /(?:month[- ]?close|period_close|закрытие\s+месяца|косвен|20|44)/i.test(corpus);
|
||||
const hasDistribution = /(?:distribution|распредел|writeoff|deferred_expense_to_writeoff)/i.test(corpus);
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred)/i.test(corpus);
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|сч[её]т\s*97|deferred)/i.test(corpus);
|
||||
const hasResidual = /(?:tail|остат|незакры|overdue|period_boundary|terminal_state_gap)/i.test(corpus);
|
||||
const hasContradiction = /(?:contradiction|invalid_transition|normal residual|нормальн)/i.test(corpus);
|
||||
const hasRbpWriteoffDoc = /(?:списани[ея]\s+рбп|rbp_writeoff|deferred_expense_document|writeoff document)/i.test(corpus);
|
||||
const hasRbpObject = /(?:rbp[_\s-]?object|объект\s+рбп|analytics|subkonto|расходыбудущихпериодов)/i.test(corpus);
|
||||
const hasMovement = /(?:movement|движен|хозрасчетный|document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasPeriodEndResidual = /(?:period_boundary|end_period|2020-07-31|остат)/i.test(corpus);
|
||||
const hasFixedAsset = /(?:fixed_asset|asset_card|объект\s+ос|основн(?:ые|ых)?\s+сред|depreciat|амортиз|account[:\s]*0[12]|\b0[12](?:\.\d{2})?\b)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasAmortizationDoc = /(?:depreciat|amortization|начислен[а-я]*\s+амортиз|документ\s+амортиз)/i.test(corpus);
|
||||
const hasExpectedFaSet = /(?:expected_fa_set|expected[_\s-]?set|find_fixed_asset_cards_expected_for_period|expected_set_seed|fa_expected_set_candidate)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasActualFaSet = /(?:actual_fa_set|find_fixed_asset_movements_accounts_01_02|fa_actual_set_candidate|seed_amortization_documents|collect_fa_object_movements)/i.test(
|
||||
corpus
|
||||
);
|
||||
const hasFaCoverageCompare = /(?:expected_vs_actual|compare_expected_vs_actual|missing_fa|coverage_compare|missing_fa_candidates)/i.test(
|
||||
corpus
|
||||
);
|
||||
|
||||
if (claimType === "prove_settlement_closure_state") {
|
||||
if (hasPayment) checks.add("payment_document_found");
|
||||
@@ -380,7 +595,7 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
||||
if (hasPosting) checks.add("posting_link_found");
|
||||
} else if (claimType === "prove_vat_chain_completeness") {
|
||||
if (/(?:document|receipt|realization|поступлен|реализац)/i.test(corpus)) checks.add("source_document_found");
|
||||
if (/(?:invoice|счет[- ]фактур)/i.test(corpus)) checks.add("invoice_found");
|
||||
if (/(?:invoice|сч[её]т[- ]?фактур)/i.test(corpus)) checks.add("invoice_found");
|
||||
if (hasRegister || hasVat) checks.add("tax_register_entry_found");
|
||||
if (hasBook) checks.add("book_entry_found");
|
||||
if (hasChain) checks.add("chain_linkage_status");
|
||||
@@ -388,6 +603,13 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
||||
if (hasMonthClose || hasClose) checks.add("close_operation_found");
|
||||
if (hasDistribution) checks.add("distribution_step_found");
|
||||
if (hasResidual) checks.add("residual_tail_found");
|
||||
} else if (claimType === "prove_fixed_asset_amortization_coverage") {
|
||||
if (hasAmortizationDoc) checks.add("amortization_document_found");
|
||||
if (hasFixedAsset) checks.add("fixed_asset_object_identified");
|
||||
if (hasExpectedFaSet) checks.add("expected_fa_set_reconstructed");
|
||||
if (hasActualFaSet || hasAmortizationDoc) checks.add("actual_fa_set_reconstructed");
|
||||
if (hasMovement || hasPosting) checks.add("movement_or_posting_link_found");
|
||||
if (hasFaCoverageCompare || (hasExpectedFaSet && hasActualFaSet)) checks.add("missing_fa_candidates_assessed");
|
||||
} else {
|
||||
if (hasRbpWriteoffDoc || (hasRbp && hasDistribution)) checks.add("rbp_writeoff_document_found");
|
||||
if (hasRbpObject || hasRbp) checks.add("rbp_object_identified");
|
||||
@@ -540,7 +762,11 @@ function buildDerivedEvidenceFromItem(input: {
|
||||
account_context: Array.isArray(input.item.account_context) ? input.item.account_context : [],
|
||||
account_debit: input.item.account_debit ?? null,
|
||||
account_credit: input.item.account_credit ?? null,
|
||||
relation_pattern_hits: Array.isArray(input.item.relation_pattern_hits) ? input.item.relation_pattern_hits : []
|
||||
relation_pattern_hits: Array.isArray(input.item.relation_pattern_hits) ? input.item.relation_pattern_hits : [],
|
||||
fa_object_hint: String(input.item.fa_object_hint ?? "").trim() || null,
|
||||
fa_expected_set_candidate: Boolean(input.item.fa_expected_set_candidate),
|
||||
fa_actual_set_candidate: Boolean(input.item.fa_actual_set_candidate),
|
||||
fa_coverage_status: String(input.item.fa_coverage_status ?? "").trim() || null
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -553,6 +779,189 @@ function buildClaimStatusTemplate(requiredChecks: string[]): Record<string, "fou
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeFaObjectToken(value: string): string | null {
|
||||
const normalized = String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (/^live movement row #\d+$/i.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized.slice(0, 140);
|
||||
}
|
||||
|
||||
function periodFromEvidence(evidence: EvidenceItem): string | null {
|
||||
const payload = toObject(evidence.payload);
|
||||
return (
|
||||
String(evidence.source_ref?.period ?? "").trim() ||
|
||||
String(evidence.pointer?.source?.period ?? "").trim() ||
|
||||
String(payload?.period ?? "").trim() ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function collectFaCoverage(input: {
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
}): {
|
||||
expectedSet: string[];
|
||||
actualSet: string[];
|
||||
missingCandidates: string[];
|
||||
uncertainCandidates: string[];
|
||||
relationMap: Array<{
|
||||
fa_object: string;
|
||||
document_amortization: string[];
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
period: string[];
|
||||
coverage_status: "covered" | "missing" | "uncertain";
|
||||
}>;
|
||||
} {
|
||||
const state = new Map<
|
||||
string,
|
||||
{
|
||||
expected: boolean;
|
||||
actual: boolean;
|
||||
movement: boolean;
|
||||
posting: boolean;
|
||||
docs: Set<string>;
|
||||
periods: Set<string>;
|
||||
}
|
||||
>();
|
||||
|
||||
const touch = (objectName: string) => {
|
||||
const key = objectName.toLowerCase();
|
||||
const existing = state.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const created = {
|
||||
expected: false,
|
||||
actual: false,
|
||||
movement: false,
|
||||
posting: false,
|
||||
docs: new Set<string>(),
|
||||
periods: new Set<string>()
|
||||
};
|
||||
state.set(key, created);
|
||||
return created;
|
||||
};
|
||||
|
||||
for (const result of input.retrievalResults) {
|
||||
const items = Array.isArray(result.items) ? result.items : [];
|
||||
for (const item of items) {
|
||||
const objectToken = normalizeFaObjectToken(
|
||||
String(item.fa_object_hint ?? item.display_name ?? item.source_id ?? "").trim()
|
||||
);
|
||||
if (!objectToken) {
|
||||
continue;
|
||||
}
|
||||
const slot = touch(objectToken);
|
||||
if (Boolean(item.fa_expected_set_candidate)) {
|
||||
slot.expected = true;
|
||||
}
|
||||
if (Boolean(item.fa_actual_set_candidate)) {
|
||||
slot.actual = true;
|
||||
}
|
||||
const corpus = JSON.stringify(item).toLowerCase();
|
||||
if (/(?:movement|движен|хозрасчет|document_to_posting)/i.test(corpus)) {
|
||||
slot.movement = true;
|
||||
}
|
||||
if (/(?:posting|проводк|account_)/i.test(corpus)) {
|
||||
slot.posting = true;
|
||||
}
|
||||
const documentContext = Array.isArray(item.document_context) ? item.document_context : [];
|
||||
for (const doc of documentContext) {
|
||||
const token = String(doc ?? "").trim();
|
||||
if (token) {
|
||||
slot.docs.add(token);
|
||||
}
|
||||
}
|
||||
const period = String(item.period ?? item.Period ?? "").trim();
|
||||
if (period) {
|
||||
slot.periods.add(period);
|
||||
}
|
||||
}
|
||||
|
||||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||
for (const evidenceItem of evidence) {
|
||||
const payload = toObject(evidenceItem.payload) ?? {};
|
||||
const objectToken = normalizeFaObjectToken(
|
||||
String(payload.fa_object_hint ?? evidenceItem.source_ref?.id ?? evidenceItem.pointer?.source?.id ?? "").trim()
|
||||
);
|
||||
if (!objectToken) {
|
||||
continue;
|
||||
}
|
||||
const slot = touch(objectToken);
|
||||
if (Boolean(payload.fa_expected_set_candidate)) {
|
||||
slot.expected = true;
|
||||
}
|
||||
if (Boolean(payload.fa_actual_set_candidate)) {
|
||||
slot.actual = true;
|
||||
}
|
||||
const corpus = JSON.stringify({
|
||||
payload,
|
||||
mechanism_note: evidenceItem.mechanism_note,
|
||||
source_ref: evidenceItem.source_ref
|
||||
}).toLowerCase();
|
||||
if (/(?:movement|движен|хозрасчет|document_to_posting)/i.test(corpus)) {
|
||||
slot.movement = true;
|
||||
}
|
||||
if (/(?:posting|проводк|account_)/i.test(corpus)) {
|
||||
slot.posting = true;
|
||||
}
|
||||
const documentContext = Array.isArray(payload.document_context) ? payload.document_context : [];
|
||||
for (const doc of documentContext) {
|
||||
const token = String(doc ?? "").trim();
|
||||
if (token) {
|
||||
slot.docs.add(token);
|
||||
}
|
||||
}
|
||||
const period = periodFromEvidence(evidenceItem);
|
||||
if (period) {
|
||||
slot.periods.add(period);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entries = Array.from(state.entries());
|
||||
const expectedSet = entries
|
||||
.filter(([, slot]) => slot.expected)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const actualSet = entries
|
||||
.filter(([, slot]) => slot.actual)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const expectedResolved = expectedSet.length > 0 ? expectedSet : actualSet;
|
||||
const missingCandidates = expectedResolved.filter((item) => !actualSet.includes(item)).slice(0, 32);
|
||||
const uncertainCandidates = entries
|
||||
.filter(([, slot]) => !slot.expected && !slot.actual)
|
||||
.map(([objectName]) => objectName)
|
||||
.slice(0, 32);
|
||||
const relationMap = entries.slice(0, 48).map(([objectName, slot]) => {
|
||||
const coverageStatus: "covered" | "missing" | "uncertain" =
|
||||
slot.expected && slot.actual ? "covered" : slot.expected && !slot.actual ? "missing" : "uncertain";
|
||||
return {
|
||||
fa_object: objectName,
|
||||
document_amortization: Array.from(slot.docs).slice(0, 4),
|
||||
movement: slot.movement,
|
||||
posting: slot.posting,
|
||||
period: Array.from(slot.periods).slice(0, 4),
|
||||
coverage_status: coverageStatus
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
expectedSet: expectedResolved,
|
||||
actualSet,
|
||||
missingCandidates,
|
||||
uncertainCandidates,
|
||||
relationMap
|
||||
};
|
||||
}
|
||||
|
||||
export function applyTargetedEvidenceAcquisition(input: {
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
claimAudit: ClaimBoundAnchorAudit;
|
||||
@@ -673,6 +1082,21 @@ export function applyTargetedEvidenceAcquisition(input: {
|
||||
reasonCodes.push("targeted_evidence_hit_rate_low");
|
||||
}
|
||||
|
||||
const faCoverage =
|
||||
input.claimAudit.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? collectFaCoverage({
|
||||
retrievalResults: adjustedResults
|
||||
})
|
||||
: null;
|
||||
if (faCoverage) {
|
||||
if (faCoverage.expectedSet.length <= 0) {
|
||||
reasonCodes.push("fa_expected_set_not_reconstructed");
|
||||
}
|
||||
if (faCoverage.actualSet.length <= 0) {
|
||||
reasonCodes.push("fa_actual_set_not_reconstructed");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
retrievalResults: adjustedResults,
|
||||
audit: {
|
||||
@@ -683,7 +1107,18 @@ export function applyTargetedEvidenceAcquisition(input: {
|
||||
targeted_evidence_hits: targetedEvidenceHits,
|
||||
targeted_evidence_hit_rate: targetedEvidenceHitRate,
|
||||
targeted_evidence_source_refs: Array.from(sourceRefs).slice(0, 24),
|
||||
...(faCoverage
|
||||
? {
|
||||
fa_expected_set: faCoverage.expectedSet,
|
||||
fa_actual_set_from_amortization: faCoverage.actualSet,
|
||||
fa_missing_candidates: faCoverage.missingCandidates,
|
||||
fa_uncertain_candidates: faCoverage.uncertainCandidates,
|
||||
fa_relation_map: faCoverage.relationMap
|
||||
}
|
||||
: {}),
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FEATURE_ASSISTANT_MCP_RUNTIME_V1,
|
||||
FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1
|
||||
} from "../config";
|
||||
import { inferP0DomainFromMessage as inferRuntimeP0DomainHint } from "./investigationState";
|
||||
|
||||
interface SnapshotLink {
|
||||
relation: string;
|
||||
@@ -73,6 +74,7 @@ interface LiveMcpCallPlan {
|
||||
call_id: string;
|
||||
purpose: string;
|
||||
query: string;
|
||||
limit?: number;
|
||||
required_for_claim: boolean;
|
||||
account_scope_override?: string[];
|
||||
}>;
|
||||
@@ -82,6 +84,7 @@ interface LiveMcpCallPlan {
|
||||
interface LiveMcpCallExecution {
|
||||
call_id: string;
|
||||
purpose: string;
|
||||
requested_limit: number;
|
||||
required_for_claim: boolean;
|
||||
status: "ok" | "empty" | "error";
|
||||
fetched_rows: number;
|
||||
@@ -197,6 +200,23 @@ const RBP_REQUIRED_LIVE_CALLS = [
|
||||
"compute_end_period_residual_by_rbp_object"
|
||||
];
|
||||
|
||||
const VAT_REQUIRED_LIVE_CALLS = [
|
||||
"find_vat_source_documents_in_period",
|
||||
"find_vat_invoice_links_in_period",
|
||||
"find_vat_register_entries_in_period",
|
||||
"find_vat_book_entries_in_period"
|
||||
];
|
||||
|
||||
const FA_REQUIRED_LIVE_CALLS = [
|
||||
"find_amortization_documents_in_period",
|
||||
"find_fixed_asset_movements_accounts_01_02",
|
||||
"find_fixed_asset_cards_expected_for_period",
|
||||
"match_expected_vs_actual_fa_coverage"
|
||||
];
|
||||
|
||||
const CLAIM_BOUND_PRIMARY_LIVE_LIMIT = Math.max(ASSISTANT_MCP_LIVE_LIMIT, 96);
|
||||
const CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT = Math.max(ASSISTANT_MCP_LIVE_LIMIT, 128);
|
||||
|
||||
function pushUniqueLine(target: string[], line: string): void {
|
||||
if (!target.includes(line)) {
|
||||
target.push(line);
|
||||
@@ -228,6 +248,13 @@ function parseFiniteNumber(value: unknown): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveLiveCallLimit(limit: unknown): number {
|
||||
if (typeof limit === "number" && Number.isFinite(limit)) {
|
||||
return Math.max(1, Math.trunc(limit));
|
||||
}
|
||||
return ASSISTANT_MCP_LIVE_LIMIT;
|
||||
}
|
||||
|
||||
function formatIsoDateUtc(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
@@ -296,9 +323,119 @@ function hasRbpSignal(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasFixedAssetAmortizationSignal(text: string): boolean {
|
||||
return /(?:амортиз|основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|depreciat|fixed\s*asset|account\s*0[12]|счет\s*0[12])/i.test(
|
||||
String(text ?? "").toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallPlan {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
|
||||
const faClaim =
|
||||
preferredDomainHint === "fixed_asset_amortization" ||
|
||||
hasFixedAssetAmortizationSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
|
||||
semanticProfile.domain_scope.includes("fixed_assets");
|
||||
if (faClaim) {
|
||||
return {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
query_subject: "fixed_asset_amortization_coverage",
|
||||
required_live_calls: [...FA_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_amortization_documents_in_period",
|
||||
purpose: "seed_amortization_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "find_fixed_asset_movements_accounts_01_02",
|
||||
purpose: "collect_fa_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "find_fixed_asset_cards_expected_for_period",
|
||||
purpose: "build_expected_fa_set",
|
||||
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
},
|
||||
{
|
||||
call_id: "match_expected_vs_actual_fa_coverage",
|
||||
purpose: "compare_expected_vs_actual_fa_coverage",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["01", "02", "08"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
const vatClaim =
|
||||
preferredDomainHint === "vat_document_register_book" ||
|
||||
semanticProfile.query_subject === "vat_chain_conflict" ||
|
||||
semanticProfile.domain_scope.includes("vat") ||
|
||||
/(?:\bvat\b|ндс|invoice|счет[- ]фактур|книга покупок|книга продаж|register)/i.test(String(fragmentText ?? "").toLowerCase());
|
||||
if (vatClaim) {
|
||||
return {
|
||||
claim_type: "prove_vat_chain_completeness",
|
||||
query_subject: "vat_chain_conflict",
|
||||
required_live_calls: [...VAT_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_vat_source_documents_in_period",
|
||||
purpose: "seed_vat_source_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_invoice_links_in_period",
|
||||
purpose: "collect_invoice_links",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_register_entries_in_period",
|
||||
purpose: "collect_vat_register_entries",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
},
|
||||
{
|
||||
call_id: "find_vat_book_entries_in_period",
|
||||
purpose: "collect_vat_book_entries",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["19", "68"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
const rbpClaim =
|
||||
(preferredDomainHint === "month_close_costs_20_44" && hasRbpSignal(fragmentText)) ||
|
||||
hasRbpSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "deferred_expense_lifecycle_anomaly" ||
|
||||
semanticProfile.domain_scope.includes("deferred_expense");
|
||||
@@ -319,46 +456,44 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
|
||||
};
|
||||
}
|
||||
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
|
||||
return {
|
||||
claim_type: "prove_rbp_tail_state",
|
||||
query_subject: "deferred_expense_lifecycle_anomaly",
|
||||
required_live_calls: [...RBP_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_rbp_writeoff_documents_in_period",
|
||||
purpose: "seed_writeoff_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "find_rbp_object_movements_account_97",
|
||||
purpose: "collect_rbp_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97"]
|
||||
},
|
||||
{
|
||||
call_id: "find_month_close_entries_linked_to_rbp",
|
||||
purpose: "link_month_close_to_rbp",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "compute_end_period_residual_by_rbp_object",
|
||||
purpose: "collect_residual_tail_signals",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
}
|
||||
],
|
||||
{
|
||||
call_id: "find_rbp_writeoff_documents_in_period",
|
||||
purpose: "seed_writeoff_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "find_rbp_object_movements_account_97",
|
||||
purpose: "collect_rbp_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97"]
|
||||
},
|
||||
{
|
||||
call_id: "find_month_close_entries_linked_to_rbp",
|
||||
purpose: "link_month_close_to_rbp",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "compute_end_period_residual_by_rbp_object",
|
||||
purpose: "collect_residual_tail_signals",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
|
||||
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
@@ -1614,11 +1749,32 @@ const WRONG_DOCUMENT_MARKERS =
|
||||
const REPEATED_ANOMALY_MARKERS =
|
||||
/(?:\u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449|\u0441\u0435\u0440\u0438\u0439\u043d|\u043f\u0430\u0442\u0442\u0435\u0440\u043d|repeat(?:ed|ability)?)/iu;
|
||||
|
||||
function inferQuerySubject(text: string, domains: string[], anomalies: string[]): string {
|
||||
function inferQuerySubject(
|
||||
text: string,
|
||||
domains: string[],
|
||||
anomalies: string[],
|
||||
preferredDomainHint: string | null
|
||||
): string {
|
||||
if (preferredDomainHint === "vat_document_register_book") {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (preferredDomainHint === "fixed_asset_amortization") {
|
||||
return "fixed_asset_card_mismatch";
|
||||
}
|
||||
if (preferredDomainHint === "month_close_costs_20_44") {
|
||||
return "period_closure_risk";
|
||||
}
|
||||
if (preferredDomainHint === "settlements_60_62") {
|
||||
return "supplier_tail_analysis";
|
||||
}
|
||||
|
||||
const lower = text.toLowerCase();
|
||||
if ((domains.includes("bank") || domains.includes("settlements")) && WRONG_DOCUMENT_MARKERS.test(lower)) {
|
||||
return "bank_settlement_mismatch";
|
||||
}
|
||||
if (domains.includes("vat")) {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (domains.includes("suppliers")) {
|
||||
return "supplier_tail_analysis";
|
||||
}
|
||||
@@ -1631,9 +1787,6 @@ function inferQuerySubject(text: string, domains: string[], anomalies: string[])
|
||||
if (domains.includes("fixed_assets")) {
|
||||
return "fixed_asset_card_mismatch";
|
||||
}
|
||||
if (domains.includes("vat")) {
|
||||
return "vat_chain_conflict";
|
||||
}
|
||||
if (domains.includes("period_close")) {
|
||||
return "period_closure_risk";
|
||||
}
|
||||
@@ -1784,9 +1937,10 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||
relationPatterns: dedupedRelations,
|
||||
anomalyPatterns: dedupedAnomalies
|
||||
});
|
||||
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
|
||||
|
||||
return {
|
||||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies),
|
||||
query_subject: inferQuerySubject(lower, dedupedDomains, dedupedAnomalies, preferredDomainHint),
|
||||
account_scope: dedupedAccounts,
|
||||
subaccount_scope: [],
|
||||
domain_scope: dedupedDomains,
|
||||
@@ -2773,11 +2927,15 @@ export class AssistantDataLayer {
|
||||
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
|
||||
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
||||
const accountScope =
|
||||
explicitAccountScope.length > 0
|
||||
livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? ["01", "02", "08"]
|
||||
: livePlan.claim_type === "prove_vat_chain_completeness"
|
||||
? ["19", "68"]
|
||||
: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? ["97", "20", "25", "26", "44"]
|
||||
: explicitAccountScope.length > 0
|
||||
? explicitAccountScope
|
||||
: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? ["97", "20", "25", "26", "44"]
|
||||
: [];
|
||||
: [];
|
||||
const callExecutions: LiveMcpCallExecution[] = [];
|
||||
const collectedRows: Array<Record<string, unknown>> = [];
|
||||
const errors: string[] = [];
|
||||
@@ -2785,6 +2943,7 @@ export class AssistantDataLayer {
|
||||
let matchedRowsTotal = 0;
|
||||
|
||||
for (const call of livePlan.calls) {
|
||||
const callLimit = resolveLiveCallLimit(call.limit);
|
||||
const callAccountScope =
|
||||
Array.isArray(call.account_scope_override) && call.account_scope_override.length > 0
|
||||
? call.account_scope_override
|
||||
@@ -2792,7 +2951,7 @@ export class AssistantDataLayer {
|
||||
try {
|
||||
const payload = await this.fetchJsonWithTimeout(endpoint, {
|
||||
query: call.query,
|
||||
limit: ASSISTANT_MCP_LIVE_LIMIT
|
||||
limit: callLimit
|
||||
});
|
||||
const parsed = this.parseExecuteQueryPayload(payload);
|
||||
if (parsed.error) {
|
||||
@@ -2800,6 +2959,7 @@ export class AssistantDataLayer {
|
||||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
@@ -2827,6 +2987,7 @@ export class AssistantDataLayer {
|
||||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: rowsForAnswer.length > 0 ? "ok" : "empty",
|
||||
fetched_rows: parsed.rows.length,
|
||||
@@ -2840,6 +3001,7 @@ export class AssistantDataLayer {
|
||||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
requested_limit: callLimit,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
@@ -2864,7 +3026,11 @@ export class AssistantDataLayer {
|
||||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
claim_type: item.claim_type
|
||||
claim_type: item.claim_type,
|
||||
fa_object_hint: item.fa_object_hint,
|
||||
fa_expected_set_candidate: item.fa_expected_set_candidate,
|
||||
fa_actual_set_candidate: item.fa_actual_set_candidate,
|
||||
fa_coverage_status: item.fa_coverage_status
|
||||
}));
|
||||
|
||||
const executedRequiredCalls = callExecutions
|
||||
@@ -2917,7 +3083,13 @@ export class AssistantDataLayer {
|
||||
route,
|
||||
channel: ASSISTANT_MCP_CHANNEL,
|
||||
proxy: ASSISTANT_MCP_PROXY_URL,
|
||||
source_profile: livePlan.claim_type ? "claim_bound_rbp_live_path" : "generic_live_probe",
|
||||
source_profile: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? "claim_bound_rbp_live_path"
|
||||
: livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
|
||||
? "claim_bound_fa_live_path"
|
||||
: livePlan.claim_type === "prove_vat_chain_completeness"
|
||||
? "claim_bound_vat_live_path"
|
||||
: "generic_live_probe",
|
||||
claim_type: livePlan.claim_type,
|
||||
query_subject: livePlan.query_subject,
|
||||
account_scope: accountScope,
|
||||
@@ -3111,29 +3283,57 @@ export class AssistantDataLayer {
|
||||
const querySubject = valueAsString(row.__query_subject ?? "").trim() || null;
|
||||
const registratorLower = registrator.toLowerCase();
|
||||
const hasRbpByDocument = /(?:рбп|deferred|списани[ея]\s+рбп)/i.test(registratorLower);
|
||||
const hasFaByDocument = /(?:амортиз|depreciat|основн(?:ые|ых)\s+сред|fixed\s*asset)/i.test(registratorLower);
|
||||
const hasAccount97 = accountContext.some((item) => /^97(?:\.|$)/.test(item));
|
||||
const hasFixedAssetAccount = accountContext.some((item) => /^(?:01|02|08)(?:\.|$)/.test(item));
|
||||
const hasCloseDoc =
|
||||
/(?:закрыти[ея]\s+месяц|period\s*close|month\s*close|close\s+operation)/i.test(registratorLower) ||
|
||||
callId.includes("month_close");
|
||||
const faExpectedSetCandidate = callId === "find_fixed_asset_cards_expected_for_period" || callPurpose === "build_expected_fa_set";
|
||||
const faActualSetCandidate =
|
||||
callId === "find_amortization_documents_in_period" ||
|
||||
callId === "find_fixed_asset_movements_accounts_01_02" ||
|
||||
callPurpose === "seed_amortization_documents" ||
|
||||
callPurpose === "collect_fa_object_movements";
|
||||
const faCoverageStatus =
|
||||
callId === "match_expected_vs_actual_fa_coverage"
|
||||
? "expected_vs_actual_compare"
|
||||
: faExpectedSetCandidate && faActualSetCandidate
|
||||
? "covered"
|
||||
: faExpectedSetCandidate
|
||||
? "expected_only"
|
||||
: faActualSetCandidate
|
||||
? "actual_only"
|
||||
: null;
|
||||
const faObjectHint =
|
||||
(registrator || "").trim() ||
|
||||
`${debit || "n/a"}|${credit || "n/a"}|${amount !== null ? amount : "n/a"}`;
|
||||
const relationPatternHits = uniqueStrings([
|
||||
"document_to_posting",
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_to_writeoff" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "asset_card_to_depreciation" : "",
|
||||
faCoverageStatus === "expected_vs_actual_compare" ? "expected_vs_actual_coverage_compare" : "",
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
callId.includes("residual") ? "residuals_zero_or_explained" : ""
|
||||
]);
|
||||
const documentContext = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_document" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "depreciation_document" : "",
|
||||
hasCloseDoc ? "period_close_document" : "",
|
||||
"posting"
|
||||
]);
|
||||
const graphDomainScope = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "fixed_asset" : "",
|
||||
hasCloseDoc ? "period_close" : ""
|
||||
]);
|
||||
const lifecycleMarkers = uniqueStrings([
|
||||
callId.includes("residual") ? "period_boundary" : "",
|
||||
callId.includes("residual") ? "tail_state_observed" : "",
|
||||
hasCloseDoc ? "close_operation" : ""
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
hasFaByDocument || hasFixedAssetAccount ? "amortization_accrual" : "",
|
||||
faExpectedSetCandidate ? "expected_set_seed" : "",
|
||||
faCoverageStatus === "expected_vs_actual_compare" ? "coverage_compare" : ""
|
||||
]);
|
||||
return {
|
||||
source_entity: "MCPLiveMovement",
|
||||
@@ -3153,6 +3353,10 @@ export class AssistantDataLayer {
|
||||
claim_type: claimType,
|
||||
query_subject: querySubject,
|
||||
amount,
|
||||
fa_object_hint: faObjectHint,
|
||||
fa_expected_set_candidate: faExpectedSetCandidate,
|
||||
fa_actual_set_candidate: faActualSetCandidate,
|
||||
fa_coverage_status: faCoverageStatus,
|
||||
source_layer: "mcp_live_probe",
|
||||
route
|
||||
};
|
||||
|
||||
@@ -5,7 +5,12 @@ import type { EvidenceItem } from "../types/stage1Contracts";
|
||||
import type { ProblemUnit } from "../types/stage2ProblemUnits";
|
||||
import type { ClaimBoundAnchorAudit } from "./assistantClaimBoundEvidence";
|
||||
|
||||
type P0DomainHint = "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44" | null;
|
||||
type P0DomainHint =
|
||||
| "settlements_60_62"
|
||||
| "vat_document_register_book"
|
||||
| "month_close_costs_20_44"
|
||||
| "fixed_asset_amortization"
|
||||
| null;
|
||||
|
||||
const JULY_YEAR = "2020";
|
||||
const JULY_MONTH = "07";
|
||||
@@ -161,10 +166,37 @@ function collectPercentLikeSpans(text: string): Array<{ start: number; end: numb
|
||||
return spans;
|
||||
}
|
||||
|
||||
function collectContractLikeSpans(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function intersectsSpan(start: number, end: number, spans: Array<{ start: number; end: number }>): boolean {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
|
||||
function hasAccountContextAround(text: string, start: number, end: number): boolean {
|
||||
const left = text.slice(Math.max(0, start - 28), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(
|
||||
`${left} ${right}`
|
||||
);
|
||||
}
|
||||
|
||||
interface AccountExtractionAudit {
|
||||
resolved_account_anchors: string[];
|
||||
raw_numeric_tokens: string[];
|
||||
@@ -181,7 +213,12 @@ function extractAccountsFromTextDetailed(text: string, options?: { forceAccountC
|
||||
const dateSpans = collectDateLikeSpans(lower);
|
||||
const amountSpans = collectAmountLikeSpans(lower);
|
||||
const percentSpans = collectPercentLikeSpans(lower);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans];
|
||||
const contractSpans = collectContractLikeSpans(lower);
|
||||
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans, ...contractSpans];
|
||||
const hasAccountingLexeme =
|
||||
/(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(
|
||||
lower
|
||||
);
|
||||
const contextualPattern =
|
||||
/(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
|
||||
let contextualMatch: RegExpExecArray | null = null;
|
||||
@@ -246,6 +283,14 @@ function extractAccountsFromTextDetailed(text: string, options?: { forceAccountC
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (intersectsSpan(start, end, contractSpans)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
classification: "other_numeric"
|
||||
});
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
@@ -254,6 +299,14 @@ function extractAccountsFromTextDetailed(text: string, options?: { forceAccountC
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountingLexeme || !hasAccountContextAround(lower, start, end)) {
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
classification: "other_numeric"
|
||||
});
|
||||
rejectedAsNonAccounts.add(token);
|
||||
continue;
|
||||
}
|
||||
accounts.add(token);
|
||||
classifiedNumericTokens.push({
|
||||
token,
|
||||
@@ -509,7 +562,7 @@ function resolveJulyAnchor(rawText: string): TemporalAnchorResolution {
|
||||
const raw = String(rawText ?? "");
|
||||
const lower = raw.toLowerCase();
|
||||
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july|РёСЋР»(?:СЏ|СЊ)?)(?:\D|$)/i);
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
|
||||
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
|
||||
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
|
||||
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
|
||||
@@ -1044,6 +1097,10 @@ function isMonthClosePrefix(prefix: string): boolean {
|
||||
return numeric >= 20 && numeric <= 44;
|
||||
}
|
||||
|
||||
function isFixedAssetPrefix(prefix: string): boolean {
|
||||
return prefix === "01" || prefix === "02" || prefix === "08";
|
||||
}
|
||||
|
||||
function expectedAccountPrefixes(input: {
|
||||
focusDomainHint: P0DomainHint;
|
||||
polarity: DomainPolarity;
|
||||
@@ -1062,6 +1119,9 @@ function expectedAccountPrefixes(input: {
|
||||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
return ["20", "25", "26", "44", "97", "01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||||
return ["01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
if (input.polarity === "supplier_payable") {
|
||||
return ["60", "51", "76"];
|
||||
@@ -1121,6 +1181,13 @@ function hasWrongDomainByAccounts(accounts: string[], focusDomainHint: P0DomainH
|
||||
if (focusDomainHint === "month_close_costs_20_44") {
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix));
|
||||
}
|
||||
if (focusDomainHint === "fixed_asset_amortization") {
|
||||
const hasFixedAsset = prefixes.some((prefix) => isFixedAssetPrefix(prefix));
|
||||
if (hasFixedAsset) {
|
||||
return false;
|
||||
}
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix) || isMonthClosePrefix(prefix));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1531,3 +1598,4 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -193,7 +193,8 @@ function hasP0ClaimSignal(claimType, focusDomainHint) {
|
||||
claim === "prove_advance_offset_state" ||
|
||||
claim === "prove_vat_chain_completeness" ||
|
||||
claim === "prove_month_close_state" ||
|
||||
claim === "prove_rbp_tail_state") {
|
||||
claim === "prove_rbp_tail_state" ||
|
||||
claim === "prove_fixed_asset_amortization_coverage") {
|
||||
return true;
|
||||
}
|
||||
return (focusDomainHint === "settlements_60_62" ||
|
||||
@@ -331,6 +332,24 @@ function collectDateSpans(text) {
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
function collectContractSpans(text) {
|
||||
const spans = [];
|
||||
const contractPatterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const contractPattern of contractPatterns) {
|
||||
let match = null;
|
||||
while ((match = contractPattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
function collectAmountSpans(text) {
|
||||
const spans = [];
|
||||
const amountPatterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||||
@@ -360,6 +379,11 @@ function collectPercentSpans(text) {
|
||||
function intersectsAnySpan(start, end, spans) {
|
||||
return spans.some((span) => start < span.end && end > span.start);
|
||||
}
|
||||
function hasAccountContextAround(text, start, end) {
|
||||
const left = text.slice(Math.max(0, start - 28), start);
|
||||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
|
||||
}
|
||||
function extractAccountTokens(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const explicitAccounts = new Set();
|
||||
@@ -432,7 +456,8 @@ function extractAccountTokens(text) {
|
||||
if (explicitAccounts.size > 0) {
|
||||
return Array.from(explicitAccounts);
|
||||
}
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||||
const contractSpans = collectContractSpans(lower);
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...contractSpans];
|
||||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower);
|
||||
if (!hasAccountingLexeme) {
|
||||
return [];
|
||||
@@ -447,6 +472,9 @@ function extractAccountTokens(text) {
|
||||
if (intersectsAnySpan(start, end, spans)) {
|
||||
continue;
|
||||
}
|
||||
if (!hasAccountContextAround(lower, start, end)) {
|
||||
continue;
|
||||
}
|
||||
const prefix = value.match(/^(\d{2})/)?.[1];
|
||||
if (!prefix || !knownAccountPrefixes.has(prefix)) {
|
||||
continue;
|
||||
@@ -735,6 +763,164 @@ function collectRbpLiveRouteAudit(input) {
|
||||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function enrichFaFragmentForLive(fragmentText, temporalGuard) {
|
||||
const base = compactWhitespace(String(fragmentText ?? ""));
|
||||
const hints = [
|
||||
"Начисление амортизации",
|
||||
"объект ОС",
|
||||
"expected set ОС",
|
||||
"счет 01/02"
|
||||
];
|
||||
const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null;
|
||||
if (effective && effective.from && effective.to) {
|
||||
hints.push(`период ${effective.from}..${effective.to}`);
|
||||
}
|
||||
const hintText = hints.filter(Boolean).join(", ");
|
||||
if (!base) {
|
||||
return hintText;
|
||||
}
|
||||
if (/амортиз|основн(?:ые|ых)\s+сред|fixed\s*asset|depreciat|счет\s*0[12]|account\s*0[12]/i.test(base)) {
|
||||
return base;
|
||||
}
|
||||
return `${base}; ${hintText}`;
|
||||
}
|
||||
function enforceFaLiveRoutePlan(input) {
|
||||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||||
return {
|
||||
executionPlan: input.executionPlan,
|
||||
audit: null
|
||||
};
|
||||
}
|
||||
const requiredLiveCalls = [
|
||||
"find_amortization_documents_in_period",
|
||||
"find_fixed_asset_movements_accounts_01_02",
|
||||
"find_fixed_asset_cards_expected_for_period",
|
||||
"match_expected_vs_actual_fa_coverage"
|
||||
];
|
||||
let routeAdjusted = 0;
|
||||
let rescuedNoRoute = 0;
|
||||
const replacedRoutes = [];
|
||||
const adjustedPlan = input.executionPlan.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return item;
|
||||
}
|
||||
if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") {
|
||||
rescuedNoRoute += 1;
|
||||
routeAdjusted += 1;
|
||||
return {
|
||||
...item,
|
||||
route: "live_mcp_drilldown",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null,
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") {
|
||||
routeAdjusted += 1;
|
||||
if (item.route && item.route !== "no_route") {
|
||||
replacedRoutes.push(String(item.route));
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
route: "hybrid_store_plus_live",
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true) {
|
||||
return {
|
||||
...item,
|
||||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return {
|
||||
executionPlan: adjustedPlan,
|
||||
audit: {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
required_live_calls: requiredLiveCalls,
|
||||
route_adjustments_applied: routeAdjusted,
|
||||
rescued_no_route_fragments: rescuedNoRoute,
|
||||
replaced_routes: Array.from(new Set(replacedRoutes)),
|
||||
route_gap_reason: routeAdjusted > 0 ? "fa_claim_bound_live_route_override_applied" : null
|
||||
}
|
||||
};
|
||||
}
|
||||
function collectFaLiveRouteAudit(input) {
|
||||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||||
return null;
|
||||
}
|
||||
const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []);
|
||||
const executed = [];
|
||||
const missing = new Set();
|
||||
const routeGaps = [];
|
||||
let matchedRowsTotal = 0;
|
||||
let returnedRowsTotal = 0;
|
||||
let fetchedRowsTotal = 0;
|
||||
for (const result of input.retrievalResults) {
|
||||
if (!result || typeof result !== "object") {
|
||||
continue;
|
||||
}
|
||||
const summary = result.summary && typeof result.summary === "object" ? result.summary : null;
|
||||
const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null;
|
||||
if (!live) {
|
||||
continue;
|
||||
}
|
||||
const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : [];
|
||||
for (const callId of requiredCalls) {
|
||||
required.add(String(callId ?? "").trim());
|
||||
}
|
||||
const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : [];
|
||||
for (const call of executedCalls) {
|
||||
if (!call || typeof call !== "object") {
|
||||
continue;
|
||||
}
|
||||
executed.push(call);
|
||||
}
|
||||
const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : [];
|
||||
for (const callId of missingCalls) {
|
||||
const token = String(callId ?? "").trim();
|
||||
if (token) {
|
||||
missing.add(token);
|
||||
}
|
||||
}
|
||||
const routeGapReason = String(live.route_gap_reason ?? "").trim();
|
||||
if (routeGapReason) {
|
||||
routeGaps.push(routeGapReason);
|
||||
}
|
||||
fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0;
|
||||
matchedRowsTotal += Number(live.matched_rows ?? 0) || 0;
|
||||
returnedRowsTotal += Number(live.returned_rows ?? 0) || 0;
|
||||
}
|
||||
const requiredList = Array.from(required).filter(Boolean);
|
||||
const executedList = executed;
|
||||
const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId));
|
||||
for (const callId of missingFromExecuted) {
|
||||
missing.add(callId);
|
||||
}
|
||||
const missingList = Array.from(missing);
|
||||
const routeGapReason = missingList.length > 0
|
||||
? "required_live_calls_not_executed"
|
||||
: matchedRowsTotal <= 0
|
||||
? "claim_live_calls_executed_but_zero_matches"
|
||||
: routeGaps[0] ?? null;
|
||||
const executionRate = requiredList.length > 0
|
||||
? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4))
|
||||
: 1;
|
||||
return {
|
||||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||||
required_live_calls: requiredList,
|
||||
executed_live_calls: executedList,
|
||||
missing_live_calls: missingList,
|
||||
route_gap_reason: routeGapReason,
|
||||
live_route_execution_rate: executionRate,
|
||||
fetched_rows_total: fetchedRowsTotal,
|
||||
matched_rows_total: matchedRowsTotal,
|
||||
returned_rows_total: returnedRowsTotal,
|
||||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function toDebugRoutes(routeSummary) {
|
||||
if (!routeSummary) {
|
||||
return [];
|
||||
@@ -1201,7 +1387,7 @@ function extractNormalizedPeriodLiteral(text) {
|
||||
}
|
||||
function extractFollowupAccountAnchorsLoose(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)];
|
||||
const anchors = [];
|
||||
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
|
||||
let match = null;
|
||||
@@ -1254,27 +1440,8 @@ function hasCrossScopeConflictWithState(userMessage, state) {
|
||||
return false;
|
||||
}
|
||||
function inferP0DomainFromMessage(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountTokens(lower);
|
||||
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
|
||||
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
|
||||
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
|
||||
const hasFixedAssetAccount = accountTokens.some((token) => /^(?:01|02|08)(?:\.|$)/.test(token));
|
||||
const vatLexical = /(?:ндс|vat|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
|
||||
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
|
||||
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат)/i.test(lower);
|
||||
const fixedAssetLexical = /(?:основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(lower);
|
||||
if (hasVatAccount || vatLexical) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (fixedAssetLexical || hasFixedAssetAccount) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
if (monthCloseLexical || hasMonthCloseAccount) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasSettlementAccount || settlementLexical) {
|
||||
return "settlements_60_62";
|
||||
if (typeof investigationState_1.inferP0DomainFromMessage === "function") {
|
||||
return investigationState_1.inferP0DomainFromMessage(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1554,13 +1721,12 @@ export class AssistantService {
|
||||
routeSummary: normalized.route_hint_summary
|
||||
});
|
||||
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
|
||||
const focusDomainForGuards = inferredDomainByMessage === "fixed_asset_amortization"
|
||||
? "month_close_costs_20_44"
|
||||
: inferredDomainByMessage === "settlements_60_62" ||
|
||||
inferredDomainByMessage === "vat_document_register_book" ||
|
||||
inferredDomainByMessage === "month_close_costs_20_44"
|
||||
? inferredDomainByMessage
|
||||
: null;
|
||||
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
|
||||
inferredDomainByMessage === "vat_document_register_book" ||
|
||||
inferredDomainByMessage === "month_close_costs_20_44" ||
|
||||
inferredDomainByMessage === "fixed_asset_amortization"
|
||||
? inferredDomainByMessage
|
||||
: null;
|
||||
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
|
||||
userMessage,
|
||||
normalized: normalized.normalized,
|
||||
@@ -1592,6 +1758,12 @@ export class AssistantService {
|
||||
temporalGuard
|
||||
});
|
||||
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||||
const faRoutePlanEnforcement = enforceFaLiveRoutePlan({
|
||||
executionPlan,
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
temporalGuard
|
||||
});
|
||||
executionPlan = faRoutePlanEnforcement.executionPlan;
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||||
const retrievalCalls = [];
|
||||
@@ -1679,6 +1851,11 @@ export class AssistantService {
|
||||
retrievalResults,
|
||||
planAudit: rbpRoutePlanEnforcement.audit
|
||||
});
|
||||
const faLiveRouteAudit = collectFaLiveRouteAudit({
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
retrievalResults,
|
||||
planAudit: faRoutePlanEnforcement.audit
|
||||
});
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||||
@@ -1792,6 +1969,7 @@ export class AssistantService {
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
@@ -1884,6 +2062,8 @@ export class AssistantService {
|
||||
claim_anchor_audit: claimAnchorAudit,
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
|
||||
@@ -112,6 +112,25 @@ function collectDateLikeSpans(text: string): Array<{ start: number; end: number
|
||||
return spans;
|
||||
}
|
||||
|
||||
function collectContractLikeSpans(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [
|
||||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function collectAmountLikeSpans(text: string): Array<{ start: number; end: number }> {
|
||||
const spans: Array<{ start: number; end: number }> = [];
|
||||
const patterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||||
@@ -154,7 +173,12 @@ function hasAccountContextAround(text: string, start: number, end: number): bool
|
||||
|
||||
function detectAccounts(text: string): string[] {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const blockedSpans = [...collectDateLikeSpans(lower), ...collectAmountLikeSpans(lower), ...collectPercentLikeSpans(lower)];
|
||||
const blockedSpans = [
|
||||
...collectDateLikeSpans(lower),
|
||||
...collectAmountLikeSpans(lower),
|
||||
...collectPercentLikeSpans(lower),
|
||||
...collectContractLikeSpans(lower)
|
||||
];
|
||||
const hasAccountingLexeme =
|
||||
/(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|расч[её]т|аванс|долг|settlement|payment|supplier|customer|ндс|vat|рбп|deferred|амортиз)/iu.test(
|
||||
lower
|
||||
@@ -217,37 +241,47 @@ function detectPeriod(text: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectExplicitDomainHint(text: string): string | null {
|
||||
export function inferP0DomainFromMessage(text: string): string | null {
|
||||
const messageCorpus = String(text ?? "").toLowerCase();
|
||||
const accounts = detectAccounts(text);
|
||||
const hasSettlementSignal =
|
||||
accounts.some((item) => isSettlementAccount(item)) ||
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
|
||||
const hasSettlementAccount = accounts.some((item) => isSettlementAccount(item));
|
||||
const hasVatAccount = accounts.some((item) => isVatAccount(item));
|
||||
const hasCloseAccount = accounts.some((item) => isCloseCostsAccount(item));
|
||||
const hasFixedAssetAccount = accounts.some((item) => isFixedAssetAccount(item));
|
||||
|
||||
const hasSettlementLexical = /(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
const hasVatLexical = /(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
const hasCloseLexical =
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
const hasExplicitFixedAssetLexical =
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
if (hasSettlementSignal) {
|
||||
const hasBroadMonthCloseLexical =
|
||||
/(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|month\s*close|period\s*close|регламентн)/i.test(messageCorpus);
|
||||
|
||||
// Keep settlement lane stable when 60/62 lexical/account anchors are explicit
|
||||
// and there is no explicit VAT intent.
|
||||
if ((hasSettlementAccount || hasSettlementLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
const hasVatSignal =
|
||||
accounts.some((item) => isVatAccount(item)) ||
|
||||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
if (hasVatSignal) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
const hasCloseSignal =
|
||||
accounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
if (hasCloseSignal) {
|
||||
if ((hasCloseAccount || hasCloseLexical || hasBroadMonthCloseLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical && !hasFixedAssetAccount) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
const hasFixedAssetSignal =
|
||||
accounts.some((item) => isFixedAssetAccount(item)) ||
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
if (hasFixedAssetSignal) {
|
||||
if (hasVatAccount || hasVatLexical) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
if (hasFixedAssetAccount || hasExplicitFixedAssetLexical) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
if (hasCloseAccount || hasCloseLexical) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasSettlementAccount || hasSettlementLexical) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -283,7 +317,7 @@ function deriveScopeOrigin(input: {
|
||||
}
|
||||
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
|
||||
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
|
||||
const explicitDomain = detectExplicitDomainHint(input.userMessage);
|
||||
const explicitDomain = inferP0DomainFromMessage(input.userMessage);
|
||||
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
|
||||
return "explicit_from_message";
|
||||
}
|
||||
@@ -406,7 +440,7 @@ function inferFollowupActiveDomain(input: {
|
||||
: messageCorpus;
|
||||
|
||||
const hasFixedAssetLexicalSignal =
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
const hasFixedAssetAccountSignal =
|
||||
@@ -414,6 +448,17 @@ function inferFollowupActiveDomain(input: {
|
||||
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
const hasBroadMonthCloseSignal =
|
||||
/(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|регламентн|month\s*close|period\s*close)/i.test(messageCorpus);
|
||||
if (
|
||||
(input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus) ||
|
||||
hasBroadMonthCloseSignal) &&
|
||||
!hasFixedAssetLexicalSignal &&
|
||||
!hasFixedAssetAccountSignal
|
||||
) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (hasFixedAssetLexicalSignal || hasFixedAssetAccountSignal) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user