Этап 4 corrective pack 2 по family isolation после текущих routing fixes

This commit is contained in:
2026-03-29 15:27:01 +03:00
parent 133b6dca3c
commit f74e7b697a
30 changed files with 4427 additions and 259 deletions
@@ -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)
}
};
}