Stage 4 / Wave 18 закрыт blocker-pack по временному якорю, полярности домена и допустимости evidence
This commit is contained in:
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user