962 lines
43 KiB
JavaScript
962 lines
43 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.resolveClaimBoundAnchors = resolveClaimBoundAnchors;
|
||
exports.applyTargetedEvidenceAcquisition = applyTargetedEvidenceAcquisition;
|
||
const nanoid_1 = require("nanoid");
|
||
function uniqueStrings(values) {
|
||
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean)));
|
||
}
|
||
function toObject(value) {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||
return null;
|
||
}
|
||
return value;
|
||
}
|
||
function normalizeTwoDigits(value) {
|
||
return String(value).padStart(2, "0");
|
||
}
|
||
function normalizeDateIso(value) {
|
||
const raw = String(value ?? "").trim();
|
||
if (!raw) {
|
||
return null;
|
||
}
|
||
const isoDay = raw.match(/\b(20\d{2})[-/.](0?[1-9]|1[0-2])[-/.](0?[1-9]|[12]\d|3[01])\b/);
|
||
if (isoDay) {
|
||
return `${isoDay[1]}-${normalizeTwoDigits(isoDay[2])}-${normalizeTwoDigits(isoDay[3])}`;
|
||
}
|
||
const isoMonth = raw.match(/\b(20\d{2})[-/.](0?[1-9]|1[0-2])\b/);
|
||
if (isoMonth) {
|
||
return `${isoMonth[1]}-${normalizeTwoDigits(isoMonth[2])}-01`;
|
||
}
|
||
const localDay = raw.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?[1-9]|1[0-2])[./-](\d{2}|\d{4})\b/);
|
||
if (localDay) {
|
||
const year = localDay[3].length === 2 ? `20${localDay[3]}` : localDay[3];
|
||
return `${year}-${normalizeTwoDigits(localDay[2])}-${normalizeTwoDigits(localDay[1])}`;
|
||
}
|
||
return null;
|
||
}
|
||
function isoToDate(value) {
|
||
const normalized = normalizeDateIso(value);
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
const date = new Date(`${normalized}T00:00:00Z`);
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
}
|
||
function formatDate(date) {
|
||
const year = date.getUTCFullYear();
|
||
const month = normalizeTwoDigits(String(date.getUTCMonth() + 1));
|
||
const day = normalizeTwoDigits(String(date.getUTCDate()));
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
function shiftDays(iso, deltaDays) {
|
||
const date = isoToDate(iso);
|
||
if (!date) {
|
||
return null;
|
||
}
|
||
date.setUTCDate(date.getUTCDate() + deltaDays);
|
||
return formatDate(date);
|
||
}
|
||
function accountPrefix(value) {
|
||
const token = String(value ?? "").trim();
|
||
const match = token.match(/^(\d{2})/);
|
||
return match ? match[1] : null;
|
||
}
|
||
function accountPrefixesFromAnchors(anchors) {
|
||
const prefixes = new Set();
|
||
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) {
|
||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||
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";
|
||
}
|
||
if (input.focusDomainHint === "fixed_asset_amortization") {
|
||
return "prove_fixed_asset_amortization_coverage";
|
||
}
|
||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||
if (hasRbpLexical || hasRbpAccount) {
|
||
return "prove_rbp_tail_state";
|
||
}
|
||
return "prove_month_close_state";
|
||
}
|
||
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) {
|
||
const lower = message.toLowerCase();
|
||
const out = [];
|
||
if (/(?:supplier|vendor|поставщик|кредитор)/i.test(lower))
|
||
out.push("supplier");
|
||
if (/(?:customer|buyer|покупатель|дебитор)/i.test(lower))
|
||
out.push("customer");
|
||
return uniqueStrings(out);
|
||
}
|
||
function detectSignals(message) {
|
||
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|книга\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) {
|
||
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, key) {
|
||
return uniqueStrings(Array.isArray(anchors?.[key]) ? anchors?.[key] : []);
|
||
}
|
||
function buildAllowedContextWindow(primaryPeriod) {
|
||
if (!primaryPeriod) {
|
||
return null;
|
||
}
|
||
const from = shiftDays(primaryPeriod.from, -365);
|
||
const to = shiftDays(primaryPeriod.to, 365);
|
||
if (!from || !to) {
|
||
return null;
|
||
}
|
||
return {
|
||
from,
|
||
to,
|
||
granularity: "month"
|
||
};
|
||
}
|
||
function missingFromRequired(required, resolved) {
|
||
const missing = [];
|
||
for (const anchor of required) {
|
||
if (anchor === "counterparty_scope_or_contract") {
|
||
if ((resolved.counterparty_scope?.length ?? 0) <= 0 && (resolved.contract?.length ?? 0) <= 0) {
|
||
missing.push(anchor);
|
||
}
|
||
continue;
|
||
}
|
||
if (anchor === "settlement_object") {
|
||
if ((resolved.contract?.length ?? 0) <= 0 && (resolved.document_numbers?.length ?? 0) <= 0) {
|
||
missing.push(anchor);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
return uniqueStrings(missing);
|
||
}
|
||
function resolveClaimBoundAnchors(input) {
|
||
const claimType = inferClaimType({
|
||
userMessage: input.userMessage,
|
||
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 = {
|
||
period: uniqueStrings([...mergeAnchors(input.companyAnchors, "periods"), ...mergeAnchors(input.companyAnchors, "dates")]),
|
||
account_scope: mergeAnchors(input.companyAnchors, "accounts"),
|
||
amounts: mergeAnchors(input.companyAnchors, "amounts"),
|
||
contract: mergeAnchors(input.companyAnchors, "contract_numbers"),
|
||
document_numbers: mergeAnchors(input.companyAnchors, "document_numbers"),
|
||
document_types: mergeAnchors(input.companyAnchors, "document_types"),
|
||
counterparty_scope: inferCounterpartyScope(input.userMessage),
|
||
advance_signal: signals.hasAdvance ? ["advance"] : [],
|
||
closure_signal: signals.hasClosure ? ["closure"] : [],
|
||
vat_signal: includeVatAnchors && hasVatSignal ? ["vat"] : [],
|
||
chain_signal: includeVatAnchors && hasVatSignal ? ["chain"] : [],
|
||
close_signal: includeMonthCloseAnchors && hasMonthCloseSignal ? ["month_close"] : [],
|
||
cost_scope: [],
|
||
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 (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]);
|
||
}
|
||
const requiredByClaim = {
|
||
prove_settlement_closure_state: ["period", "account_scope", "counterparty_scope_or_contract", "closure_signal"],
|
||
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_fixed_asset_amortization_coverage: [
|
||
"period",
|
||
"fixed_asset_signal",
|
||
"amortization_signal",
|
||
"amount_or_document",
|
||
"account_scope_or_document_type"
|
||
]
|
||
};
|
||
const requiredAnchors = requiredByClaim[claimType];
|
||
const missingAnchors = missingFromRequired(requiredAnchors, resolvedAnchors);
|
||
const resolutionRate = requiredAnchors.length > 0
|
||
? Number(((requiredAnchors.length - missingAnchors.length) / requiredAnchors.length).toFixed(4))
|
||
: 1;
|
||
const allowedContextWindow = buildAllowedContextWindow(input.primaryPeriod ?? null);
|
||
const reasonCodes = [];
|
||
if (missingAnchors.length > 0) {
|
||
reasonCodes.push("claim_missing_required_anchors");
|
||
}
|
||
if (resolutionRate < 0.8) {
|
||
reasonCodes.push("claim_anchor_resolution_low");
|
||
}
|
||
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,
|
||
claim_anchor_resolution_rate: resolutionRate,
|
||
primary_period: input.primaryPeriod ?? null,
|
||
allowed_context_window: allowedContextWindow,
|
||
context_expansion_reasons_allowed: [
|
||
"prehistory",
|
||
"carryover",
|
||
"post_period_closure",
|
||
"long_running_contract_context"
|
||
],
|
||
reason_codes: uniqueStrings(reasonCodes)
|
||
};
|
||
}
|
||
function buildCorpusFromItem(item) {
|
||
return JSON.stringify({
|
||
source_entity: item.source_entity,
|
||
source_id: item.source_id,
|
||
period: item.period ?? item.Period,
|
||
account_context: item.account_context,
|
||
account_debit: item.account_debit,
|
||
account_credit: item.account_credit,
|
||
document_context: item.document_context,
|
||
relation_pattern_hits: item.relation_pattern_hits,
|
||
graph_domain_scope: item.graph_domain_scope,
|
||
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();
|
||
}
|
||
function buildCorpusFromEvidence(evidence) {
|
||
return JSON.stringify({
|
||
source_ref: evidence.source_ref,
|
||
pointer: evidence.pointer,
|
||
payload: evidence.payload,
|
||
mechanism_note: evidence.mechanism_note,
|
||
limitation: evidence.limitation
|
||
}).toLowerCase();
|
||
}
|
||
function requiredChecksByClaim(claimType) {
|
||
if (claimType === "prove_settlement_closure_state") {
|
||
return [
|
||
"payment_document_found",
|
||
"contract_matched",
|
||
"settlement_object_matched",
|
||
"closing_document_found",
|
||
"register_closure_entry_found",
|
||
"posting_link_found"
|
||
];
|
||
}
|
||
if (claimType === "prove_advance_offset_state") {
|
||
return [
|
||
"payment_document_found",
|
||
"advance_marker_found",
|
||
"settlement_object_matched",
|
||
"closing_document_found",
|
||
"register_closure_entry_found",
|
||
"posting_link_found"
|
||
];
|
||
}
|
||
if (claimType === "prove_vat_chain_completeness") {
|
||
return ["source_document_found", "invoice_found", "tax_register_entry_found", "book_entry_found", "chain_linkage_status"];
|
||
}
|
||
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",
|
||
"rbp_movement_found",
|
||
"rbp_period_end_residual_found",
|
||
"rbp_writeoff_lifecycle_confirmed",
|
||
"residual_tail_found",
|
||
"close_contradiction_or_normal_residual"
|
||
];
|
||
}
|
||
function detectChecksForCorpus(corpus, claimType, anchors) {
|
||
const checks = new Set();
|
||
const hasContractAnchor = (anchors.contract ?? []).some((token) => token.length >= 3 && corpus.includes(String(token).toLowerCase())) ||
|
||
/(?:contract|договор)/i.test(corpus);
|
||
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 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 = /(?:книг[аи](?:\s+)?(?:покупок|продаж)|book)/i.test(corpus);
|
||
const hasChain = /(?:chain|link|document_to_posting|invoice_to_vat|связ)/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 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");
|
||
if (hasContractAnchor)
|
||
checks.add("contract_matched");
|
||
if (hasSettlementAccount)
|
||
checks.add("settlement_object_matched");
|
||
if (hasClose)
|
||
checks.add("closing_document_found");
|
||
if (hasRegister)
|
||
checks.add("register_closure_entry_found");
|
||
if (hasPosting)
|
||
checks.add("posting_link_found");
|
||
}
|
||
else if (claimType === "prove_advance_offset_state") {
|
||
if (hasPayment)
|
||
checks.add("payment_document_found");
|
||
if (hasAdvance)
|
||
checks.add("advance_marker_found");
|
||
if (hasSettlementAccount)
|
||
checks.add("settlement_object_matched");
|
||
if (hasClose)
|
||
checks.add("closing_document_found");
|
||
if (hasRegister)
|
||
checks.add("register_closure_entry_found");
|
||
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 (hasRegister || hasVat)
|
||
checks.add("tax_register_entry_found");
|
||
if (hasBook)
|
||
checks.add("book_entry_found");
|
||
if (hasChain)
|
||
checks.add("chain_linkage_status");
|
||
}
|
||
else if (claimType === "prove_month_close_state") {
|
||
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");
|
||
if (hasMovement)
|
||
checks.add("rbp_movement_found");
|
||
if (hasPeriodEndResidual || hasResidual)
|
||
checks.add("rbp_period_end_residual_found");
|
||
if (hasRbp && hasDistribution)
|
||
checks.add("rbp_writeoff_lifecycle_confirmed");
|
||
if (hasResidual)
|
||
checks.add("residual_tail_found");
|
||
if (hasContradiction || hasClose)
|
||
checks.add("close_contradiction_or_normal_residual");
|
||
}
|
||
return Array.from(checks);
|
||
}
|
||
function hasAnchorLink(corpus, claimAudit) {
|
||
const values = Object.values(claimAudit.resolved_anchors).flat();
|
||
return values.some((token) => {
|
||
const value = String(token ?? "").toLowerCase().trim();
|
||
if (value.length < 2)
|
||
return false;
|
||
return corpus.includes(value);
|
||
});
|
||
}
|
||
function resolveContextExpansionDecision(input) {
|
||
if (!input.period || !input.claimAudit.primary_period) {
|
||
return { allowed: true, reason: null, inside_primary_period: true };
|
||
}
|
||
const normalized = normalizeDateIso(input.period);
|
||
if (!normalized) {
|
||
return { allowed: false, reason: null, inside_primary_period: false };
|
||
}
|
||
const primaryFrom = normalizeDateIso(input.claimAudit.primary_period.from);
|
||
const primaryTo = normalizeDateIso(input.claimAudit.primary_period.to);
|
||
if (!primaryFrom || !primaryTo) {
|
||
return { allowed: true, reason: null, inside_primary_period: true };
|
||
}
|
||
if (normalized >= primaryFrom && normalized <= primaryTo) {
|
||
return { allowed: true, reason: null, inside_primary_period: true };
|
||
}
|
||
const allowedFrom = normalizeDateIso(input.claimAudit.allowed_context_window?.from ?? "");
|
||
const allowedTo = normalizeDateIso(input.claimAudit.allowed_context_window?.to ?? "");
|
||
if (allowedFrom && normalized < allowedFrom) {
|
||
return { allowed: false, reason: null, inside_primary_period: false };
|
||
}
|
||
if (allowedTo && normalized > allowedTo) {
|
||
return { allowed: false, reason: null, inside_primary_period: false };
|
||
}
|
||
const linked = hasAnchorLink(input.corpus, input.claimAudit) || input.matchedChecks.length > 0;
|
||
const fromDate = isoToDate(primaryFrom);
|
||
const toDate = isoToDate(primaryTo);
|
||
const curDate = isoToDate(normalized);
|
||
const hasContractAnchor = (input.claimAudit.resolved_anchors.contract?.length ?? 0) > 0;
|
||
if (!fromDate || !toDate || !curDate) {
|
||
return { allowed: linked, reason: linked ? "carryover" : null, inside_primary_period: false };
|
||
}
|
||
const diffBefore = Math.floor((fromDate.getTime() - curDate.getTime()) / (24 * 3600 * 1000));
|
||
const diffAfter = Math.floor((curDate.getTime() - toDate.getTime()) / (24 * 3600 * 1000));
|
||
if (curDate < fromDate) {
|
||
if (linked && hasContractAnchor && diffBefore > 31) {
|
||
return { allowed: true, reason: "long_running_contract_context", inside_primary_period: false };
|
||
}
|
||
if (linked) {
|
||
return { allowed: true, reason: "prehistory", inside_primary_period: false };
|
||
}
|
||
if (diffBefore <= 31) {
|
||
return { allowed: true, reason: "carryover", inside_primary_period: false };
|
||
}
|
||
return { allowed: false, reason: null, inside_primary_period: false };
|
||
}
|
||
if (curDate > toDate) {
|
||
if (diffAfter <= 31) {
|
||
return { allowed: true, reason: "carryover", inside_primary_period: false };
|
||
}
|
||
if (linked && hasContractAnchor) {
|
||
return { allowed: true, reason: "long_running_contract_context", inside_primary_period: false };
|
||
}
|
||
if (linked) {
|
||
return { allowed: true, reason: "post_period_closure", inside_primary_period: false };
|
||
}
|
||
return { allowed: false, reason: null, inside_primary_period: false };
|
||
}
|
||
return { allowed: true, reason: null, inside_primary_period: true };
|
||
}
|
||
function evidenceSourceNamespaceFromItem(item) {
|
||
const sourceLayer = String(item.source_layer ?? "").toLowerCase();
|
||
if (sourceLayer.includes("snapshot")) {
|
||
return "snapshot_2020";
|
||
}
|
||
return "assistant_derived";
|
||
}
|
||
function buildDerivedEvidenceFromItem(input) {
|
||
const sourceEntity = String(input.item.source_entity ?? "unknown");
|
||
const sourceId = String(input.item.source_id ?? `derived-${(0, nanoid_1.nanoid)(8)}`);
|
||
const period = String(input.item.period ?? input.item.Period ?? "").trim() || null;
|
||
const namespace = evidenceSourceNamespaceFromItem(input.item);
|
||
const canonical = `evidence_source_ref_v1|${namespace}|${sourceEntity.toLowerCase()}|${sourceId.toLowerCase()}|${String(period ?? "").toLowerCase()}`;
|
||
const confidence = input.matchedChecks.length >= 2 ? "high" : "medium";
|
||
return {
|
||
evidence_id: `claim-ev-${(0, nanoid_1.nanoid)(10)}`,
|
||
claim_ref: `claim:${input.claimType}`,
|
||
source_type: "derived",
|
||
source_ref: {
|
||
schema_version: "evidence_source_ref_v1",
|
||
namespace,
|
||
entity: sourceEntity,
|
||
id: sourceId,
|
||
period,
|
||
canonical_ref: canonical
|
||
},
|
||
pointer: {
|
||
fragment_id: input.result.fragment_id,
|
||
route: input.result.route,
|
||
source: {
|
||
namespace,
|
||
entity: sourceEntity,
|
||
id: sourceId,
|
||
period
|
||
},
|
||
locator: {
|
||
field_path: null,
|
||
item_index: null
|
||
}
|
||
},
|
||
evidence_kind: "mechanism_link",
|
||
mechanism_note: input.matchedChecks[0] ?? null,
|
||
confidence,
|
||
limitation: null,
|
||
payload: {
|
||
from_targeted_item: true,
|
||
claim_type: input.claimType,
|
||
claim_target_checks: input.matchedChecks,
|
||
context_expansion_allowed: input.expansion.allowed,
|
||
context_expansion_reason: input.expansion.reason,
|
||
period,
|
||
source_entity: sourceEntity,
|
||
source_id: sourceId,
|
||
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 : [],
|
||
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
|
||
}
|
||
};
|
||
}
|
||
function buildClaimStatusTemplate(requiredChecks) {
|
||
const out = {};
|
||
for (const check of requiredChecks) {
|
||
out[check] = "not_found";
|
||
}
|
||
return out;
|
||
}
|
||
function normalizeFaObjectToken(value) {
|
||
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) {
|
||
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) {
|
||
const state = new Map();
|
||
const touch = (objectName) => {
|
||
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(),
|
||
periods: new Set()
|
||
};
|
||
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 = 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
|
||
};
|
||
}
|
||
function applyTargetedEvidenceAcquisition(input) {
|
||
const requiredChecks = requiredChecksByClaim(input.claimAudit.claim_type);
|
||
const checkStatus = buildClaimStatusTemplate(requiredChecks);
|
||
let targetedItemHits = 0;
|
||
let targetedEvidenceHits = 0;
|
||
const sourceRefs = new Set();
|
||
const adjustedResults = input.retrievalResults.map((result) => {
|
||
const items = Array.isArray(result.items) ? result.items : [];
|
||
const targetedItems = [];
|
||
const derivedEvidence = [];
|
||
for (const item of items) {
|
||
const corpus = buildCorpusFromItem(item);
|
||
const matchedChecks = detectChecksForCorpus(corpus, input.claimAudit.claim_type, input.claimAudit.resolved_anchors);
|
||
for (const check of matchedChecks) {
|
||
if (check in checkStatus)
|
||
checkStatus[check] = "found";
|
||
}
|
||
if (matchedChecks.length <= 0) {
|
||
continue;
|
||
}
|
||
targetedItemHits += 1;
|
||
const expansion = resolveContextExpansionDecision({
|
||
period: String(item.period ?? item.Period ?? "").trim() || null,
|
||
claimAudit: input.claimAudit,
|
||
corpus,
|
||
matchedChecks
|
||
});
|
||
const enrichedItem = {
|
||
...item,
|
||
claim_target_checks: matchedChecks,
|
||
context_expansion_allowed: expansion.allowed,
|
||
context_expansion_reason: expansion.reason
|
||
};
|
||
targetedItems.push(enrichedItem);
|
||
if (derivedEvidence.length < 8) {
|
||
const evidence = buildDerivedEvidenceFromItem({
|
||
result,
|
||
item: enrichedItem,
|
||
claimType: input.claimAudit.claim_type,
|
||
matchedChecks,
|
||
expansion
|
||
});
|
||
derivedEvidence.push(evidence);
|
||
sourceRefs.add(evidence.source_ref.canonical_ref);
|
||
}
|
||
}
|
||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||
const targetedEvidence = [];
|
||
for (const evidenceItem of evidence) {
|
||
const corpus = buildCorpusFromEvidence(evidenceItem);
|
||
const matchedChecks = detectChecksForCorpus(corpus, input.claimAudit.claim_type, input.claimAudit.resolved_anchors);
|
||
for (const check of matchedChecks) {
|
||
if (check in checkStatus)
|
||
checkStatus[check] = "found";
|
||
}
|
||
if (matchedChecks.length <= 0) {
|
||
continue;
|
||
}
|
||
const payload = toObject(evidenceItem.payload) ?? {};
|
||
const expansion = resolveContextExpansionDecision({
|
||
period: String(evidenceItem.source_ref?.period ?? "").trim() ||
|
||
String(evidenceItem.pointer?.source?.period ?? "").trim() ||
|
||
String(payload.period ?? "").trim() ||
|
||
null,
|
||
claimAudit: input.claimAudit,
|
||
corpus,
|
||
matchedChecks
|
||
});
|
||
targetedEvidence.push({
|
||
...evidenceItem,
|
||
payload: {
|
||
...payload,
|
||
claim_type: input.claimAudit.claim_type,
|
||
claim_target_checks: matchedChecks,
|
||
context_expansion_allowed: expansion.allowed,
|
||
context_expansion_reason: expansion.reason
|
||
}
|
||
});
|
||
}
|
||
const mergedEvidence = [...targetedEvidence, ...derivedEvidence];
|
||
targetedEvidenceHits += mergedEvidence.length;
|
||
for (const item of mergedEvidence) {
|
||
sourceRefs.add(item.source_ref.canonical_ref);
|
||
}
|
||
const summary = {
|
||
...(toObject(result.summary) ?? {}),
|
||
claim_bound_targeting: {
|
||
claim_type: input.claimAudit.claim_type,
|
||
required_checks: requiredChecks,
|
||
targeted_items: targetedItems.length,
|
||
targeted_evidence: mergedEvidence.length,
|
||
derived_evidence_added: derivedEvidence.length
|
||
}
|
||
};
|
||
return {
|
||
...result,
|
||
items: targetedItems.length > 0 ? targetedItems : items,
|
||
evidence: mergedEvidence.length > 0 ? mergedEvidence : evidence,
|
||
summary
|
||
};
|
||
});
|
||
const foundChecks = Object.values(checkStatus).filter((status) => status === "found").length;
|
||
const targetedEvidenceHitRate = requiredChecks.length > 0 ? Number((foundChecks / requiredChecks.length).toFixed(4)) : 0;
|
||
const reasonCodes = [];
|
||
if (targetedEvidenceHits <= 0) {
|
||
reasonCodes.push("targeted_evidence_not_found");
|
||
}
|
||
if (targetedEvidenceHitRate < 0.8) {
|
||
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: {
|
||
claim_type: input.claimAudit.claim_type,
|
||
required_checks: requiredChecks,
|
||
check_status: checkStatus,
|
||
targeted_item_hits: targetedItemHits,
|
||
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)
|
||
}
|
||
};
|
||
}
|