Stage 4 / Wave 18 закрыт blocker-pack по временному якорю, полярности домена и допустимости evidence

This commit is contained in:
2026-03-29 01:37:38 +03:00
parent d7e145010b
commit 60d8b96a14
16 changed files with 2405 additions and 146 deletions
@@ -0,0 +1,673 @@
import { nanoid } from "nanoid";
import type { UnifiedRetrievalResult } from "../types/assistant";
import type { EvidenceItem, EvidenceSourceNamespace } from "../types/stage1Contracts";
import type { CompanyAnchorSet } from "./companyAnchorResolver";
export type ClaimType =
| "prove_settlement_closure_state"
| "prove_advance_offset_state"
| "prove_vat_chain_completeness"
| "prove_month_close_state"
| "prove_rbp_tail_state";
export type ContextExpansionReason =
| "prehistory"
| "carryover"
| "post_period_closure"
| "long_running_contract_context";
export interface TemporalWindow {
from: string;
to: string;
granularity: "day" | "month";
}
export interface ClaimBoundAnchorAudit {
claim_type: ClaimType;
required_anchors: string[];
resolved_anchors: Record<string, string[]>;
missing_anchors: string[];
claim_anchor_resolution_rate: number;
primary_period: TemporalWindow | null;
allowed_context_window: TemporalWindow | null;
context_expansion_reasons_allowed: ContextExpansionReason[];
reason_codes: string[];
}
export interface TargetedEvidenceAcquisitionAudit {
claim_type: ClaimType;
required_checks: string[];
check_status: Record<string, "found" | "not_found">;
targeted_item_hits: number;
targeted_evidence_hits: number;
targeted_evidence_hit_rate: number;
targeted_evidence_source_refs: string[];
reason_codes: string[];
}
interface ContextExpansionDecision {
allowed: boolean;
reason: ContextExpansionReason | null;
inside_primary_period: boolean;
}
function uniqueStrings(values: string[]): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean)));
}
function toObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function normalizeTwoDigits(value: string): string {
return String(value).padStart(2, "0");
}
function normalizeDateIso(value: string): string | null {
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: string): Date | null {
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: Date): string {
const year = date.getUTCFullYear();
const month = normalizeTwoDigits(String(date.getUTCMonth() + 1));
const day = normalizeTwoDigits(String(date.getUTCDate()));
return `${year}-${month}-${day}`;
}
function shiftDays(iso: string, deltaDays: number): string | null {
const date = isoToDate(iso);
if (!date) {
return null;
}
date.setUTCDate(date.getUTCDate() + deltaDays);
return formatDate(date);
}
function inferClaimType(input: { userMessage: string; focusDomainHint?: string | 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) {
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";
}
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) {
return "prove_month_close_state";
}
const isAdvance = /(?:advance|аванс|offset|зачет|62\.02|60\.02)/i.test(lower);
if (isAdvance) {
return "prove_advance_offset_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 (/(?:customer|buyer|покупатель|дебитор)/i.test(lower)) out.push("customer");
return uniqueStrings(out);
}
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)
};
}
function mergeAnchors(anchors: CompanyAnchorSet | null | undefined, key: keyof CompanyAnchorSet): string[] {
return uniqueStrings(Array.isArray(anchors?.[key]) ? (anchors?.[key] as string[]) : []);
}
function buildAllowedContextWindow(primaryPeriod: TemporalWindow | null): TemporalWindow | null {
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: string[], resolved: Record<string, string[]>): string[] {
const missing: string[] = [];
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 ((resolved[anchor]?.length ?? 0) <= 0) {
missing.push(anchor);
}
}
return uniqueStrings(missing);
}
export function resolveClaimBoundAnchors(input: {
userMessage: string;
companyAnchors?: CompanyAnchorSet | null;
focusDomainHint?: string | null;
primaryPeriod?: TemporalWindow | null;
}): ClaimBoundAnchorAudit {
const claimType = inferClaimType({
userMessage: input.userMessage,
focusDomainHint: input.focusDomainHint
});
const signals = detectSignals(input.userMessage);
const resolvedAnchors: Record<string, string[]> = {
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: signals.hasVat ? ["vat"] : [],
chain_signal: signals.hasVat ? ["chain"] : [],
close_signal: signals.hasMonthClose ? ["month_close"] : [],
cost_scope: [],
rbp_signal: signals.hasRbp ? ["rbp"] : [],
writeoff_signal: signals.hasRbp ? ["writeoff"] : []
};
if (/(?:^|[^\d])(20|44)(?:[^\d]|$)/.test((resolvedAnchors.account_scope ?? []).join(" ")) || signals.hasMonthClose) {
resolvedAnchors.cost_scope = ["20_44"];
}
if (input.primaryPeriod) {
resolvedAnchors.period = uniqueStrings([...(resolvedAnchors.period ?? []), input.primaryPeriod.from, input.primaryPeriod.to]);
}
const requiredByClaim: Record<ClaimType, string[]> = {
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"]
};
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: string[] = [];
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");
}
return {
claim_type: claimType,
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: Record<string, unknown>): string {
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
}).toLowerCase();
}
function buildCorpusFromEvidence(evidence: EvidenceItem): string {
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: ClaimType): string[] {
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"];
}
return ["rbp_writeoff_lifecycle_confirmed", "residual_tail_found", "close_contradiction_or_normal_residual"];
}
function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Record<string, string[]>): string[] {
const checks = new Set<string>();
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 = /(?:книгипокупок|книгипродаж|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 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);
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 (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: string, claimAudit: ClaimBoundAnchorAudit): boolean {
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: {
period: string | null;
claimAudit: ClaimBoundAnchorAudit;
corpus: string;
matchedChecks: string[];
}): ContextExpansionDecision {
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: Record<string, unknown>): EvidenceSourceNamespace {
const sourceLayer = String(item.source_layer ?? "").toLowerCase();
if (sourceLayer.includes("snapshot")) {
return "snapshot_2020";
}
return "assistant_derived";
}
function buildDerivedEvidenceFromItem(input: {
result: UnifiedRetrievalResult;
item: Record<string, unknown>;
claimType: ClaimType;
matchedChecks: string[];
expansion: ContextExpansionDecision;
}): EvidenceItem {
const sourceEntity = String(input.item.source_entity ?? "unknown");
const sourceId = String(input.item.source_id ?? `derived-${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-${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 : []
}
};
}
function buildClaimStatusTemplate(requiredChecks: string[]): Record<string, "found" | "not_found"> {
const out: Record<string, "found" | "not_found"> = {};
for (const check of requiredChecks) {
out[check] = "not_found";
}
return out;
}
export function applyTargetedEvidenceAcquisition(input: {
retrievalResults: UnifiedRetrievalResult[];
claimAudit: ClaimBoundAnchorAudit;
}): {
retrievalResults: UnifiedRetrievalResult[];
audit: TargetedEvidenceAcquisitionAudit;
} {
const requiredChecks = requiredChecksByClaim(input.claimAudit.claim_type);
const checkStatus = buildClaimStatusTemplate(requiredChecks);
let targetedItemHits = 0;
let targetedEvidenceHits = 0;
const sourceRefs = new Set<string>();
const adjustedResults = input.retrievalResults.map((result) => {
const items = Array.isArray(result.items) ? result.items : [];
const targetedItems: Array<Record<string, unknown>> = [];
const derivedEvidence: EvidenceItem[] = [];
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: EvidenceItem[] = [];
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: string[] = [];
if (targetedEvidenceHits <= 0) {
reasonCodes.push("targeted_evidence_not_found");
}
if (targetedEvidenceHitRate < 0.8) {
reasonCodes.push("targeted_evidence_hit_rate_low");
}
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),
reason_codes: uniqueStrings(reasonCodes)
}
};
}
@@ -1,8 +1,9 @@
import type { UnifiedRetrievalResult } from "../types/assistant";
import type { UnifiedRetrievalResult } from "../types/assistant";
import type { NormalizedPayload } from "../types/normalizer";
import type { CompanyAnchorSet } from "./companyAnchorResolver";
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;
@@ -20,31 +21,73 @@ interface TemporalWindow {
granularity: "day" | "month";
}
const KNOWN_ACCOUNT_PREFIXES = new Set([
"01",
"02",
"07",
"08",
"10",
"13",
"19",
"20",
"21",
"23",
"25",
"26",
"28",
"29",
"41",
"43",
"44",
"45",
"50",
"51",
"52",
"55",
"57",
"58",
"60",
"62",
"66",
"67",
"68",
"69",
"70",
"71",
"73",
"76",
"90",
"91",
"94",
"96",
"97"
]);
const RUS_MONTH_TO_NUMBER: Record<string, string> = {
января: "01",
январь: "01",
февраля: "02",
февраль: "02",
марта: "03",
март: "03",
апреля: "04",
апрель: "04",
мая: "05",
май: "05",
июня: "06",
июнь: "06",
июля: "07",
июль: "07",
августа: "08",
август: "08",
сентября: "09",
сентябрь: "09",
октября: "10",
октябрь: "10",
ноября: "11",
ноябрь: "11",
декабря: "12",
декабрь: "12"
"\u044f\u043d\u0432\u0430\u0440\u044f": "01",
"\u044f\u043d\u0432\u0430\u0440\u044c": "01",
"\u0444\u0435\u0432\u0440\u0430\u043b\u044f": "02",
"\u0444\u0435\u0432\u0440\u0430\u043b\u044c": "02",
"\u043c\u0430\u0440\u0442\u0430": "03",
"\u043c\u0430\u0440\u0442": "03",
"\u0430\u043f\u0440\u0435\u043b\u044f": "04",
"\u0430\u043f\u0440\u0435\u043b\u044c": "04",
"\u043c\u0430\u044f": "05",
"\u043c\u0430\u0439": "05",
"\u0438\u044e\u043d\u044f": "06",
"\u0438\u044e\u043d\u044c": "06",
"\u0438\u044e\u043b\u044f": "07",
"\u0438\u044e\u043b\u044c": "07",
"\u0430\u0432\u0433\u0443\u0441\u0442\u0430": "08",
"\u0430\u0432\u0433\u0443\u0441\u0442": "08",
"\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f": "09",
"\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c": "09",
"\u043e\u043a\u0442\u044f\u0431\u0440\u044f": "10",
"\u043e\u043a\u0442\u044f\u0431\u0440\u044c": "10",
"\u043d\u043e\u044f\u0431\u0440\u044f": "11",
"\u043d\u043e\u044f\u0431\u0440\u044c": "11",
"\u0434\u0435\u043a\u0430\u0431\u0440\u044f": "12",
"\u0434\u0435\u043a\u0430\u0431\u0440\u044c": "12"
};
function uniqueStrings(values: string[]): string[] {
@@ -75,7 +118,7 @@ function extractAccountsFromText(text: string): string[] {
const lower = String(text ?? "").toLowerCase();
const accounts = new Set<string>();
const contextualPattern =
/(?:\b(?:сч(?:е|ё)т(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
/(?:\b(?:СЃС‡(?:Рµ|С‘)С‚(?:Р°|Сѓ|РѕРј|РѕРІ)?|account|schet)\b\s*(?:в„–|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
let contextualMatch: RegExpExecArray | null = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const token = String(contextualMatch[1] ?? "").trim();
@@ -91,6 +134,16 @@ function extractAccountsFromText(text: string): string[] {
if (left) accounts.add(left);
if (right) accounts.add(right);
}
const genericAccountPattern = /\b(\d{2}(?:\.\d{2})?)\b/g;
let genericMatch: RegExpExecArray | null = null;
while ((genericMatch = genericAccountPattern.exec(lower)) !== null) {
const token = String(genericMatch[1] ?? "").trim();
const prefix = token.match(/^(\d{2})/)?.[1] ?? null;
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
continue;
}
accounts.add(token);
}
return Array.from(accounts);
}
@@ -155,7 +208,7 @@ function parseDateLike(raw: string): string | null {
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
}
const rusMonthYear = value.match(
/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
);
if (rusMonthYear) {
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
@@ -195,6 +248,38 @@ function isPeriodWithinWindow(periodIso: string, window: TemporalWindow): boolea
return normalized >= window.from && normalized <= window.to;
}
function shiftIsoDay(iso: string, deltaDays: number): string | null {
const normalized = normalizeEvidenceDate(iso);
if (!normalized) {
return null;
}
const date = new Date(`${normalized}T00:00:00Z`);
if (Number.isNaN(date.getTime())) {
return null;
}
date.setUTCDate(date.getUTCDate() + deltaDays);
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
const day = String(date.getUTCDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function buildAllowedContextWindow(primaryWindow: TemporalWindow | null): TemporalWindow | null {
if (!primaryWindow) {
return null;
}
const from = shiftIsoDay(primaryWindow.from, -365);
const to = shiftIsoDay(primaryWindow.to, 365);
if (!from || !to) {
return null;
}
return {
from,
to,
granularity: "month"
};
}
function extractNormalizedFragments(normalized: NormalizedPayload | null | undefined): Array<Record<string, unknown>> {
if (!normalized || typeof normalized !== "object") {
return [];
@@ -222,7 +307,7 @@ function normalizedAnchorFromFragments(normalized: NormalizedPayload | null | un
source: `normalized_time_scope:${type || "unknown"}`
};
}
if (/(?:июл|july)/i.test(value)) {
if (/(?:июл|july|РёСЋР»)/i.test(value)) {
return {
value: `${JULY_YEAR}-${JULY_MONTH}`,
source: `normalized_time_scope:${type || "unknown"}`
@@ -254,9 +339,9 @@ 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 monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower);
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
return {
@@ -273,7 +358,7 @@ function resolveJulyAnchor(rawText: string): TemporalAnchorResolution {
const applyGuard = anchorYear === JULY_YEAR;
if (!applyGuard) {
return {
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? (monthByNamed ? "июль" : "07"),
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? (monthByNamed ? "июль" : "07"),
resolved: normalizeDateIso({
year: anchorYear,
month: JULY_MONTH,
@@ -322,6 +407,10 @@ export interface TemporalGuardAudit {
temporal_guard_applied: boolean;
temporal_guard_outcome: TemporalGuardOutcome;
primary_period_window: TemporalWindow | null;
allowed_context_window: TemporalWindow | null;
controlled_temporal_expansion_enabled: boolean;
context_expansion_reasons_allowed: Array<"prehistory" | "carryover" | "post_period_closure" | "long_running_contract_context">;
normalized_anchor_drift_detected: boolean;
reason_codes: string[];
}
@@ -342,17 +431,23 @@ export function resolveTemporalGuard(input: {
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
allowed_context_window: null,
controlled_temporal_expansion_enabled: false,
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
normalized_anchor_drift_detected: false,
reason_codes: []
};
}
let outcome: TemporalGuardOutcome = "passed";
let normalizedAnchorDriftDetected = false;
if (normalizedAnchor.value && julyAnchor.window && !isPeriodWithinWindow(normalizedAnchor.value, julyAnchor.window)) {
outcome = "failed_out_of_snapshot_window";
reasonCodes.push("normalized_anchor_out_of_snapshot_window");
normalizedAnchorDriftDetected = true;
reasonCodes.push("normalized_anchor_out_of_primary_window_overridden");
} else if (!normalizedAnchor.value && !julyAnchor.resolved) {
outcome = "ambiguous_limited";
reasonCodes.push("missing_time_anchor_under_snapshot_lock");
}
const allowedContextWindow = buildAllowedContextWindow(julyAnchor.window);
return {
raw_time_anchor: julyAnchor.raw,
resolved_time_anchor: julyAnchor.resolved ?? normalizedAnchor.value,
@@ -360,6 +455,10 @@ export function resolveTemporalGuard(input: {
temporal_guard_applied: true,
temporal_guard_outcome: outcome,
primary_period_window: julyAnchor.window,
allowed_context_window: allowedContextWindow,
controlled_temporal_expansion_enabled: true,
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
normalized_anchor_drift_detected: normalizedAnchorDriftDetected,
reason_codes: reasonCodes
};
}
@@ -375,14 +474,14 @@ export function applyTemporalHintToExecutionPlan<
}
const hint =
temporal.primary_period_window?.granularity === "day" && temporal.resolved_time_anchor
? `в рамках company snapshot даты ${temporal.resolved_time_anchor}`
: `в рамках company snapshot июля 2020 (${JULY_WINDOW.from}..${JULY_WINDOW.to})`;
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
: `primary period July 2020 (${JULY_WINDOW.from}..${JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (/2020-07|июл|july/i.test(text)) {
if (/2020-07|июл|РёСЋР»|july/i.test(text)) {
return item;
}
return {
@@ -422,7 +521,7 @@ export function resolveDomainPolarityGuard(input: {
prefixes.has("62") ||
prefixes.has("51") ||
prefixes.has("76") ||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т)/i.test(lower);
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|С…РІРѕСЃС‚)/i.test(lower);
if (!settlementSignal) {
return {
applied: false,
@@ -438,13 +537,13 @@ export function resolveDomainPolarityGuard(input: {
};
}
const supplierScore =
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable)/i.test(lower) ? 2 : 0) +
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
(prefixes.has("60") ? 2 : 0) +
(/(?:счет\s*60|по\s*60)/i.test(lower) ? 1 : 0);
(/(?:сч[её]т\s*60|по\s*60|счет\s*60|РїРѕ\s*60)/i.test(lower) ? 1 : 0);
const customerScore =
(/(?:покупат|customer|buyer|дебитор|receivable)/i.test(lower) ? 2 : 0) +
(/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
(prefixes.has("62") ? 2 : 0) +
(/(?:счет\s*62|по\s*62)/i.test(lower) ? 1 : 0);
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0);
let polarity: DomainPolarity = "mixed_or_unresolved";
if (supplierScore > 0 && customerScore === 0) {
@@ -478,17 +577,17 @@ export function applyPolarityHintToExecutionPlan<
}
const hint =
polarity.polarity === "supplier_payable"
? "контекст: расчеты с поставщиком, обязательство, счет 60"
: "контекст: расчеты с покупателем, дебиторская задолженность, счет 62";
? "context: supplier settlement, payable, account 60"
: "context: customer settlement, receivable, account 62";
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|счет\s*60|по\s*60)/i.test(text)) {
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) {
return item;
}
if (polarity.polarity === "customer_receivable" && /(покупат|customer|счет\s*62|по\s*62)/i.test(text)) {
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62|РїРѕ\s*62)/i.test(text)) {
return item;
}
return {
@@ -499,11 +598,11 @@ export function applyPolarityHintToExecutionPlan<
}
function containsReceivableSignal(value: string): boolean {
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
}
function containsPayableSignal(value: string): boolean {
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
}
function problemUnitCorpus(unit: ProblemUnit): string {
@@ -786,6 +885,32 @@ function liveAccountScopeWasApplied(result: UnifiedRetrievalResult): boolean {
return Array.isArray(accountScope) && accountScope.length > 0;
}
function evidenceContextExpansionMeta(evidence: EvidenceItem): {
allowed: boolean;
reason: string | null;
} {
const payload = toObject(evidence.payload);
const allowed = Boolean(payload?.context_expansion_allowed);
const reason = String(payload?.context_expansion_reason ?? "").trim() || null;
return { allowed, reason };
}
function itemContextExpansionMeta(item: Record<string, unknown>): {
allowed: boolean;
reason: string | null;
} {
const allowed = Boolean(item.context_expansion_allowed);
const reason = String(item.context_expansion_reason ?? "").trim() || null;
return { allowed, reason };
}
function withinAllowedContextWindow(normalizedPeriod: string, temporal: TemporalGuardAudit): boolean {
if (!temporal.allowed_context_window) {
return false;
}
return normalizedPeriod >= temporal.allowed_context_window.from && normalizedPeriod <= temporal.allowed_context_window.to;
}
function evidenceAdmissibilityReasons(input: {
evidence: EvidenceItem;
temporal: TemporalGuardAudit;
@@ -803,10 +928,16 @@ function evidenceAdmissibilityReasons(input: {
const period = extractEvidencePeriod(input.evidence);
if (period && input.temporal.primary_period_window) {
const normalized = normalizeEvidenceDate(period);
if (normalized && normalized > input.temporal.primary_period_window.to) {
reasons.add("future_dated_or_out_of_window");
} else if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
reasons.add("wrong_period");
const expansionMeta = evidenceContextExpansionMeta(input.evidence);
if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
const insideAllowed = withinAllowedContextWindow(normalized, input.temporal);
if (insideAllowed && expansionMeta.allowed && expansionMeta.reason) {
// Allowed controlled temporal expansion: period is outside primary but linked and explained.
} else if (normalized > input.temporal.primary_period_window.to && !insideAllowed) {
reasons.add("future_dated_or_out_of_window");
} else {
reasons.add("wrong_period");
}
}
}
const accounts = evidenceAccounts(input.evidence);
@@ -854,10 +985,16 @@ function itemRejectReasons(input: {
const period = itemPeriod(input.item);
if (period && input.temporal.primary_period_window) {
const normalized = normalizeEvidenceDate(period);
if (normalized && normalized > input.temporal.primary_period_window.to) {
reasons.add("future_dated_or_out_of_window");
} else if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
reasons.add("wrong_period");
const expansionMeta = itemContextExpansionMeta(input.item);
if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
const insideAllowed = withinAllowedContextWindow(normalized, input.temporal);
if (insideAllowed && expansionMeta.allowed && expansionMeta.reason) {
// Allowed controlled temporal expansion: period is outside primary but linked and explained.
} else if (normalized > input.temporal.primary_period_window.to && !insideAllowed) {
reasons.add("future_dated_or_out_of_window");
} else {
reasons.add("wrong_period");
}
}
}
const accounts = itemAccounts(input.item);
@@ -924,7 +1061,9 @@ export function applyEvidenceAdmissibilityGate(input: {
continue;
}
const limitationCode = String(item.limitation?.reason_code ?? "").trim();
if (!limitationCode && item.confidence !== "low") {
const payload = toObject(item.payload);
const expandedByContext = Boolean(payload?.context_expansion_reason);
if (!limitationCode && item.confidence !== "low" && !expandedByContext) {
categoryBreakdown.hard_evidence += 1;
} else {
categoryBreakdown.supporting_signal += 1;
@@ -1008,9 +1147,13 @@ export interface GroundedAnswerEligibilityAudit {
eligible: boolean;
temporal_passed: boolean;
polarity_passed: boolean;
claim_anchors_passed: boolean;
claim_anchor_resolution_rate: number | null;
missing_required_anchors: number;
admissible_evidence_count: number;
critical_contradiction: boolean;
outcome: "grounded_allowed" | "limited_or_insufficient_evidence";
grounding_mode: "grounded_positive" | "limited_or_insufficient_evidence";
reason_codes: string[];
}
@@ -1018,13 +1161,32 @@ export function evaluateGroundedAnswerEligibility(input: {
temporal: TemporalGuardAudit;
polarity: DomainPolarityGuardAudit;
evidence: EvidenceAdmissibilityAudit;
claimAnchors?: ClaimBoundAnchorAudit | null;
targetedEvidenceHitRate?: number | null;
}): GroundedAnswerEligibilityAudit {
const temporalPassed = input.temporal.temporal_guard_outcome === "passed";
const polarityPassed =
!input.polarity.applied || input.polarity.outcome === "passed" || input.polarity.outcome === "not_applicable";
const claimAnchorResolutionRate = input.claimAnchors ? Number(input.claimAnchors.claim_anchor_resolution_rate ?? 0) : null;
const missingRequiredAnchors = input.claimAnchors ? Number(input.claimAnchors.missing_anchors?.length ?? 0) : 0;
const requiredAnchorsCount = input.claimAnchors ? Number(input.claimAnchors.required_anchors?.length ?? 0) : 0;
const claimAnchorsPassed =
!input.claimAnchors ||
((claimAnchorResolutionRate ?? 1) >= 0.5 &&
missingRequiredAnchors <= Math.max(1, Math.floor(Math.max(requiredAnchorsCount, 1) / 2)));
const admissibleEvidenceCount = input.evidence.admissible_evidence_count;
const criticalContradiction = Boolean(input.polarity.critical_contradiction);
const eligible = temporalPassed && polarityPassed && admissibleEvidenceCount > 0 && !criticalContradiction;
const targetedEvidencePassed =
input.targetedEvidenceHitRate == null || Number.isNaN(Number(input.targetedEvidenceHitRate))
? true
: Number(input.targetedEvidenceHitRate) > 0;
const eligible =
temporalPassed &&
polarityPassed &&
claimAnchorsPassed &&
admissibleEvidenceCount > 0 &&
targetedEvidencePassed &&
!criticalContradiction;
const reasonCodes: string[] = [];
if (!temporalPassed) {
reasonCodes.push(`temporal_guard_${input.temporal.temporal_guard_outcome}`);
@@ -1032,9 +1194,15 @@ export function evaluateGroundedAnswerEligibility(input: {
if (!polarityPassed) {
reasonCodes.push(`polarity_guard_${input.polarity.outcome}`);
}
if (!claimAnchorsPassed) {
reasonCodes.push("claim_anchor_coverage_insufficient");
}
if (admissibleEvidenceCount <= 0) {
reasonCodes.push("admissible_evidence_count_zero");
}
if (!targetedEvidencePassed) {
reasonCodes.push("targeted_evidence_hit_rate_zero");
}
if (criticalContradiction) {
reasonCodes.push("critical_domain_or_account_contradiction");
}
@@ -1042,9 +1210,13 @@ export function evaluateGroundedAnswerEligibility(input: {
eligible,
temporal_passed: temporalPassed,
polarity_passed: polarityPassed,
claim_anchors_passed: claimAnchorsPassed,
claim_anchor_resolution_rate: claimAnchorResolutionRate,
missing_required_anchors: missingRequiredAnchors,
admissible_evidence_count: admissibleEvidenceCount,
critical_contradiction: criticalContradiction,
outcome: eligible ? "grounded_allowed" : "limited_or_insufficient_evidence",
grounding_mode: eligible ? "grounded_positive" : "limited_or_insufficient_evidence",
reason_codes: uniqueStrings(reasonCodes)
};
}
@@ -1057,14 +1229,18 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
return groundingCheck;
}
const status =
eligibility.admissible_evidence_count <= 0 || !eligibility.temporal_passed ? "no_grounded_answer" : "partial";
eligibility.admissible_evidence_count <= 0 || !eligibility.temporal_passed || !eligibility.claim_anchors_passed
? "no_grounded_answer"
: "partial";
const reasonMap: Record<string, string> = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре."
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
@@ -1076,3 +1252,4 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
reasons: uniqueStrings(reasons)
};
}
@@ -11,6 +11,7 @@ import * as retrievalResultNormalizer_1 from "./retrievalResultNormalizer";
import * as questionTypeResolver_1 from "./questionTypeResolver";
import * as companyAnchorResolver_1 from "./companyAnchorResolver";
import * as assistantRuntimeGuards_1 from "./assistantRuntimeGuards";
import * as assistantClaimBoundEvidence_1 from "./assistantClaimBoundEvidence";
function retrievalSummaryForRoute(route) {
if (route === "store_canonical")
return "Canonical accounting data path selected.";
@@ -1207,6 +1208,12 @@ export class AssistantService {
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const claimAnchorAudit = (0, assistantClaimBoundEvidence_1.resolveClaimBoundAnchors)({
userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards,
primaryPeriod: temporalGuard.primary_period_window
});
const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
let executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
@@ -1277,6 +1284,11 @@ export class AssistantService {
guard: domainPolarityGuardInitial
});
retrievalResults = polarityGuardResult.retrievalResults;
const targetedEvidenceResult = (0, assistantClaimBoundEvidence_1.applyTargetedEvidenceAcquisition)({
retrievalResults,
claimAudit: claimAnchorAudit
});
retrievalResults = targetedEvidenceResult.retrievalResults;
const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({
retrievalResults,
temporal: temporalGuard,
@@ -1291,7 +1303,9 @@ export class AssistantService {
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
temporal: temporalGuard,
polarity: polarityGuardResult.audit,
evidence: evidenceGateResult.audit
evidence: evidenceGateResult.audit,
claimAnchors: claimAnchorAudit,
targetedEvidenceHitRate: targetedEvidenceResult.audit.targeted_evidence_hit_rate
});
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
const focusDomainHint = followupBinding.usage?.applied
@@ -1377,6 +1391,8 @@ export class AssistantService {
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
domain_polarity_guard: polarityGuardResult.audit,
claim_anchor_audit: claimAnchorAudit,
targeted_evidence_acquisition: targetedEvidenceResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
@@ -1451,6 +1467,8 @@ export class AssistantService {
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
domain_polarity_guard: polarityGuardResult.audit,
claim_anchor_audit: claimAnchorAudit,
targeted_evidence_acquisition: targetedEvidenceResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
@@ -84,6 +84,59 @@ export interface TemporalGuardDebug {
to: string;
granularity: "day" | "month";
} | null;
allowed_context_window: {
from: string;
to: string;
granularity: "day" | "month";
} | null;
controlled_temporal_expansion_enabled: boolean;
context_expansion_reasons_allowed: Array<
"prehistory" | "carryover" | "post_period_closure" | "long_running_contract_context"
>;
normalized_anchor_drift_detected: boolean;
reason_codes: string[];
}
export interface ClaimBoundAnchorAuditDebug {
claim_type:
| "prove_settlement_closure_state"
| "prove_advance_offset_state"
| "prove_vat_chain_completeness"
| "prove_month_close_state"
| "prove_rbp_tail_state";
required_anchors: string[];
resolved_anchors: Record<string, string[]>;
missing_anchors: string[];
claim_anchor_resolution_rate: number;
primary_period: {
from: string;
to: string;
granularity: "day" | "month";
} | null;
allowed_context_window: {
from: string;
to: string;
granularity: "day" | "month";
} | null;
context_expansion_reasons_allowed: Array<
"prehistory" | "carryover" | "post_period_closure" | "long_running_contract_context"
>;
reason_codes: string[];
}
export interface TargetedEvidenceAcquisitionDebug {
claim_type:
| "prove_settlement_closure_state"
| "prove_advance_offset_state"
| "prove_vat_chain_completeness"
| "prove_month_close_state"
| "prove_rbp_tail_state";
required_checks: string[];
check_status: Record<string, "found" | "not_found">;
targeted_item_hits: number;
targeted_evidence_hits: number;
targeted_evidence_hit_rate: number;
targeted_evidence_source_refs: string[];
reason_codes: string[];
}
@@ -121,9 +174,13 @@ export interface GroundedAnswerEligibilityGuardDebug {
eligible: boolean;
temporal_passed: boolean;
polarity_passed: boolean;
claim_anchors_passed: boolean;
claim_anchor_resolution_rate: number | null;
missing_required_anchors: number;
admissible_evidence_count: number;
critical_contradiction: boolean;
outcome: "grounded_allowed" | "limited_or_insufficient_evidence";
grounding_mode: "grounded_positive" | "limited_or_insufficient_evidence";
reason_codes: string[];
}
@@ -196,6 +253,8 @@ export interface AssistantDebugPayload {
temporal_guard_outcome?: TemporalGuardDebug["temporal_guard_outcome"];
temporal_guard?: TemporalGuardDebug;
domain_polarity_guard?: DomainPolarityGuardDebug;
claim_anchor_audit?: ClaimBoundAnchorAuditDebug;
targeted_evidence_acquisition?: TargetedEvidenceAcquisitionDebug;
evidence_admissibility_gate?: EvidenceAdmissibilityGateDebug;
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
followup_state_usage?: FollowupStateUsageDebug;