NODEDC_1C/llm_normalizer/backend/dist/services/assistantRuntimeGuards.js

888 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveTemporalGuard = resolveTemporalGuard;
exports.applyTemporalHintToExecutionPlan = applyTemporalHintToExecutionPlan;
exports.resolveDomainPolarityGuard = resolveDomainPolarityGuard;
exports.applyPolarityHintToExecutionPlan = applyPolarityHintToExecutionPlan;
exports.applyDomainPolarityGuardToRetrievalResults = applyDomainPolarityGuardToRetrievalResults;
exports.applyEvidenceAdmissibilityGate = applyEvidenceAdmissibilityGate;
exports.evaluateGroundedAnswerEligibility = evaluateGroundedAnswerEligibility;
exports.applyEligibilityToGroundingCheck = applyEligibilityToGroundingCheck;
const JULY_YEAR = "2020";
const JULY_MONTH = "07";
const JULY_WINDOW = {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
};
const RUS_MONTH_TO_NUMBER = {
января: "01",
январь: "01",
февраля: "02",
февраль: "02",
марта: "03",
март: "03",
апреля: "04",
апрель: "04",
мая: "05",
май: "05",
июня: "06",
июнь: "06",
июля: "07",
июль: "07",
августа: "08",
август: "08",
сентября: "09",
сентябрь: "09",
октября: "10",
октябрь: "10",
ноября: "11",
ноябрь: "11",
декабря: "12",
декабрь: "12"
};
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 toObjectArray(value) {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item) => Boolean(item) && typeof item === "object");
}
function accountPrefix(value) {
const token = String(value ?? "").trim();
const match = token.match(/^(\d{2})/);
return match ? match[1] : null;
}
function extractAccountsFromText(text) {
const lower = String(text ?? "").toLowerCase();
const accounts = new Set();
const contextualPattern = /(?:\b(?:сч(?:е|ё)т(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const token = String(contextualMatch[1] ?? "").trim();
if (token) {
accounts.add(token);
}
}
const pairPattern = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/g;
let pairMatch = null;
while ((pairMatch = pairPattern.exec(lower)) !== null) {
const left = String(pairMatch[1] ?? "").trim();
const right = String(pairMatch[2] ?? "").trim();
if (left)
accounts.add(left);
if (right)
accounts.add(right);
}
return Array.from(accounts);
}
function extractAccountsFromUnknown(value) {
if (Array.isArray(value)) {
return uniqueStrings(value.flatMap((item) => extractAccountsFromUnknown(item)));
}
if (value && typeof value === "object") {
return uniqueStrings(Object.values(value).flatMap((item) => extractAccountsFromUnknown(item)));
}
if (typeof value !== "string" && typeof value !== "number") {
return [];
}
const text = String(value);
const matches = text.match(/\b\d{2}(?:\.\d{2})?\b/g) ?? [];
return uniqueStrings(matches);
}
function normalizeTwoDigits(value) {
return String(value).padStart(2, "0");
}
function parseYear(raw) {
const token = String(raw ?? "").trim();
if (token.length === 2) {
return `20${token}`;
}
return token;
}
function normalizeDateIso(input) {
const year = String(input.year ?? "").trim();
const month = normalizeTwoDigits(input.month ?? "");
if (!/^\d{4}$/.test(year) || !/^\d{2}$/.test(month)) {
return null;
}
if (input.day === undefined || input.day === null || String(input.day).trim().length === 0) {
return `${year}-${month}`;
}
const day = normalizeTwoDigits(input.day ?? "");
if (!/^\d{2}$/.test(day)) {
return null;
}
return `${year}-${month}-${day}`;
}
function parseDateLike(raw) {
const value = String(raw ?? "").trim().toLowerCase();
if (!value) {
return null;
}
const isoDay = value.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01])\b/);
if (isoDay) {
return normalizeDateIso({ year: isoDay[1], month: isoDay[2], day: isoDay[3] });
}
const isoMonth = value.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
if (isoMonth) {
return normalizeDateIso({ year: isoMonth[1], month: isoMonth[2] });
}
const dayMonthYear = value.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?[1-9]|1[0-2])[./-](\d{2}|\d{4})\b/);
if (dayMonthYear) {
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
}
const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i);
if (rusMonthYear) {
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
if (!month)
return null;
return normalizeDateIso({ year: rusMonthYear[2], month });
}
return null;
}
function monthStart(iso) {
const month = String(iso ?? "").slice(0, 7);
if (!/^\d{4}-\d{2}$/.test(month)) {
return null;
}
return `${month}-01`;
}
function normalizeEvidenceDate(value) {
const parsed = parseDateLike(value);
if (!parsed) {
return null;
}
if (/^\d{4}-\d{2}-\d{2}$/.test(parsed)) {
return parsed;
}
if (/^\d{4}-\d{2}$/.test(parsed)) {
return monthStart(parsed);
}
return null;
}
function isPeriodWithinWindow(periodIso, window) {
const normalized = normalizeEvidenceDate(periodIso);
if (!normalized) {
return false;
}
return normalized >= window.from && normalized <= window.to;
}
function extractNormalizedFragments(normalized) {
if (!normalized || typeof normalized !== "object") {
return [];
}
const source = normalized;
return toObjectArray(source.fragments);
}
function normalizedAnchorFromFragments(normalized) {
const fragments = extractNormalizedFragments(normalized);
for (const fragment of fragments) {
const timeScope = toObject(fragment.time_scope);
const type = String(timeScope?.type ?? "").trim().toLowerCase();
const value = String(timeScope?.value ?? "").trim();
if (!value) {
continue;
}
const parsed = parseDateLike(value);
if (parsed) {
return {
value: parsed,
source: `normalized_time_scope:${type || "unknown"}`
};
}
if (/(?:июл|july)/i.test(value)) {
return {
value: `${JULY_YEAR}-${JULY_MONTH}`,
source: `normalized_time_scope:${type || "unknown"}`
};
}
}
return {
value: null,
source: "normalized_time_scope:missing"
};
}
function collectRawTemporalAnchorText(userMessage, companyAnchors) {
return [userMessage, ...(companyAnchors?.periods ?? []), ...(companyAnchors?.dates ?? [])]
.map((item) => String(item ?? "").trim())
.filter(Boolean)
.join(" ");
}
function resolveJulyAnchor(rawText) {
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 dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
const monthByNamed = /(июл|july)/i.test(lower);
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
return {
raw: null,
resolved: null,
source: "no_july_anchor",
window: null,
applyGuard: false
};
}
const dayValue = dayByNamedJuly?.[1] ?? dayByNumeric?.[1] ?? null;
const explicitDayYear = dayByNumeric?.[3] ? parseYear(dayByNumeric[3]) : null;
const anchorYear = explicitDayYear ?? explicitYear ?? JULY_YEAR;
const applyGuard = anchorYear === JULY_YEAR;
if (!applyGuard) {
return {
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? (monthByNamed ? "июль" : "07"),
resolved: normalizeDateIso({
year: anchorYear,
month: JULY_MONTH,
...(dayValue ? { day: dayValue } : {})
}),
source: "explicit_non_snapshot_year",
window: null,
applyGuard: false
};
}
if (dayValue) {
const dayIso = normalizeDateIso({
year: JULY_YEAR,
month: JULY_MONTH,
day: dayValue
});
if (dayIso) {
return {
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? null,
resolved: dayIso,
source: "company_snapshot_july_day_lock",
window: {
from: dayIso,
to: dayIso,
granularity: "day"
},
applyGuard: true
};
}
}
return {
raw: monthByNamed ? "июль" : "2020-07",
resolved: `${JULY_YEAR}-${JULY_MONTH}`,
source: "company_snapshot_july_month_lock",
window: JULY_WINDOW,
applyGuard: true
};
}
function resolveTemporalGuard(input) {
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
const reasonCodes = [];
if (!julyAnchor.applyGuard) {
return {
raw_time_anchor: julyAnchor.raw,
resolved_time_anchor: normalizedAnchor.value,
temporal_resolution_source: normalizedAnchor.source,
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
reason_codes: []
};
}
let outcome = "passed";
if (normalizedAnchor.value && julyAnchor.window && !isPeriodWithinWindow(normalizedAnchor.value, julyAnchor.window)) {
outcome = "failed_out_of_snapshot_window";
reasonCodes.push("normalized_anchor_out_of_snapshot_window");
}
else if (!normalizedAnchor.value && !julyAnchor.resolved) {
outcome = "ambiguous_limited";
reasonCodes.push("missing_time_anchor_under_snapshot_lock");
}
return {
raw_time_anchor: julyAnchor.raw,
resolved_time_anchor: julyAnchor.resolved ?? normalizedAnchor.value,
temporal_resolution_source: julyAnchor.source,
temporal_guard_applied: true,
temporal_guard_outcome: outcome,
primary_period_window: julyAnchor.window,
reason_codes: reasonCodes
};
}
function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
if (!temporal.temporal_guard_applied) {
return executionPlan;
}
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})`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (/2020-07|июл|july/i.test(text)) {
return item;
}
return {
...item,
fragment_text: `${text}; ${hint}`.trim()
};
});
}
function resolveDomainPolarityGuard(input) {
const lower = String(input.userMessage ?? "").toLowerCase();
const accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(lower)]);
const prefixes = new Set(accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item)));
const settlementSignal = input.focusDomainHint === "settlements_60_62" ||
prefixes.has("60") ||
prefixes.has("62") ||
prefixes.has("51") ||
prefixes.has("76") ||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т)/i.test(lower);
if (!settlementSignal) {
return {
applied: false,
polarity: "not_applicable",
outcome: "not_applicable",
supplier_score: 0,
customer_score: 0,
account_scope: accounts,
rejected_problem_units: 0,
rejected_evidence: 0,
critical_contradiction: false,
reason_codes: []
};
}
const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable)/i.test(lower) ? 2 : 0) +
(prefixes.has("60") ? 2 : 0) +
(/(?:счет\s*60|по\s*60)/i.test(lower) ? 1 : 0);
const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable)/i.test(lower) ? 2 : 0) +
(prefixes.has("62") ? 2 : 0) +
(/(?:счет\s*62|по\s*62)/i.test(lower) ? 1 : 0);
let polarity = "mixed_or_unresolved";
if (supplierScore > 0 && customerScore === 0) {
polarity = "supplier_payable";
}
else if (customerScore > 0 && supplierScore === 0) {
polarity = "customer_receivable";
}
const unresolved = polarity === "mixed_or_unresolved";
return {
applied: true,
polarity,
outcome: unresolved ? "limited_unresolved_polarity" : "passed",
supplier_score: supplierScore,
customer_score: customerScore,
account_scope: accounts,
rejected_problem_units: 0,
rejected_evidence: 0,
critical_contradiction: unresolved,
reason_codes: unresolved ? ["unresolved_supplier_customer_polarity"] : []
};
}
function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
if (!polarity.applied || polarity.polarity === "mixed_or_unresolved" || polarity.polarity === "not_applicable") {
return executionPlan;
}
const hint = polarity.polarity === "supplier_payable"
? "контекст: расчеты с поставщиком, обязательство, счет 60"
: "контекст: расчеты с покупателем, дебиторская задолженность, счет 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)) {
return item;
}
if (polarity.polarity === "customer_receivable" && /(покупат|customer|счет\s*62|по\s*62)/i.test(text)) {
return item;
}
return {
...item,
fragment_text: `${text}; ${hint}`.trim()
};
});
}
function containsReceivableSignal(value) {
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
}
function containsPayableSignal(value) {
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
}
function problemUnitCorpus(unit) {
return [
unit.lifecycle_domain ?? "",
unit.problem_unit_type,
unit.business_defect_class ?? "",
unit.failed_expected_edge ?? "",
unit.invalid_transition ?? "",
unit.mechanism_summary ?? "",
unit.business_lifecycle_interpretation ?? "",
...(unit.affected_accounts ?? [])
]
.join(" ")
.toLowerCase();
}
function isProblemUnitCompatible(unit, polarity) {
if (polarity === "supplier_payable") {
return !containsReceivableSignal(problemUnitCorpus(unit));
}
if (polarity === "customer_receivable") {
return !containsPayableSignal(problemUnitCorpus(unit));
}
return true;
}
function evidenceCorpus(evidence) {
return JSON.stringify({
limitation: evidence.limitation,
payload: evidence.payload,
mechanism: evidence.mechanism_note,
source_ref: evidence.source_ref,
pointer: evidence.pointer
}).toLowerCase();
}
function evidenceAccounts(evidence) {
const payload = toObject(evidence.payload);
const direct = uniqueStrings([
...extractAccountsFromUnknown(payload?.account_context),
...extractAccountsFromUnknown(payload?.account_debit),
...extractAccountsFromUnknown(payload?.account_credit)
]);
if (direct.length > 0) {
return direct;
}
return uniqueStrings([
...extractAccountsFromUnknown(evidence.payload),
...extractAccountsFromUnknown(evidence.pointer ?? null)
]);
}
function isEvidenceCompatibleWithPolarity(evidence, polarity) {
const corpus = evidenceCorpus(evidence);
const accounts = evidenceAccounts(evidence).map((item) => accountPrefix(item)).filter((item) => Boolean(item));
if (polarity === "supplier_payable") {
if (containsReceivableSignal(corpus)) {
return false;
}
if (accounts.length > 0 && accounts.every((item) => item === "62")) {
return false;
}
}
if (polarity === "customer_receivable") {
if (containsPayableSignal(corpus)) {
return false;
}
if (accounts.length > 0 && accounts.every((item) => item === "60")) {
return false;
}
}
return true;
}
function applyDomainPolarityGuardToRetrievalResults(input) {
if (!input.guard.applied || input.guard.polarity === "not_applicable" || input.guard.polarity === "mixed_or_unresolved") {
return {
retrievalResults: input.retrievalResults,
audit: input.guard
};
}
let rejectedProblemUnits = 0;
let rejectedEvidence = 0;
let criticalContradiction = false;
const adjusted = input.retrievalResults.map((result) => {
const originalUnits = Array.isArray(result.problem_units) ? result.problem_units : [];
const filteredUnits = originalUnits.filter((unit) => isProblemUnitCompatible(unit, input.guard.polarity));
rejectedProblemUnits += Math.max(0, originalUnits.length - filteredUnits.length);
const originalEvidence = Array.isArray(result.evidence) ? result.evidence : [];
const filteredEvidence = originalEvidence.filter((item) => isEvidenceCompatibleWithPolarity(item, input.guard.polarity));
rejectedEvidence += Math.max(0, originalEvidence.length - filteredEvidence.length);
if (originalUnits.length > 0 && filteredUnits.length === 0 && originalEvidence.length > 0 && filteredEvidence.length === 0) {
criticalContradiction = true;
}
return {
...result,
evidence: filteredEvidence,
...(Array.isArray(result.problem_units)
? {
problem_units: filteredUnits
}
: {})
};
});
const reasonCodes = [];
if (rejectedProblemUnits > 0) {
reasonCodes.push("polarity_problem_unit_filter_applied");
}
if (rejectedEvidence > 0) {
reasonCodes.push("polarity_evidence_filter_applied");
}
if (criticalContradiction) {
reasonCodes.push("critical_domain_polarity_contradiction");
}
return {
retrievalResults: adjusted,
audit: {
...input.guard,
rejected_problem_units: rejectedProblemUnits,
rejected_evidence: rejectedEvidence,
critical_contradiction: criticalContradiction,
outcome: criticalContradiction ? "blocked_conflict" : "passed",
reason_codes: uniqueStrings([...(input.guard.reason_codes ?? []), ...reasonCodes])
}
};
}
function initRejectBreakdown() {
return {
wrong_period: 0,
wrong_domain: 0,
wrong_account_scope: 0,
weak_source_mapping: 0,
zero_live_match: 0,
future_dated_or_out_of_window: 0
};
}
function isVatPrefix(prefix) {
return prefix === "19" || prefix === "68";
}
function isSettlementPrefix(prefix) {
return prefix === "51" || prefix === "60" || prefix === "62" || prefix === "76";
}
function isMonthClosePrefix(prefix) {
const numeric = Number(prefix);
if (prefix === "97") {
return true;
}
if (!Number.isFinite(numeric)) {
return false;
}
return numeric >= 20 && numeric <= 44;
}
function expectedAccountPrefixes(input) {
const explicit = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(input.userMessage)])
.map((item) => accountPrefix(item))
.filter((item) => Boolean(item));
if (explicit.length > 0) {
return uniqueStrings(explicit);
}
if (input.focusDomainHint === "vat_document_register_book") {
return ["19", "68"];
}
if (input.focusDomainHint === "month_close_costs_20_44") {
return ["20", "25", "26", "44", "97", "01", "02", "08"];
}
if (input.focusDomainHint === "settlements_60_62") {
if (input.polarity === "supplier_payable") {
return ["60", "51", "76"];
}
if (input.polarity === "customer_receivable") {
return ["62", "51"];
}
return ["60", "62", "51", "76"];
}
return [];
}
function isLiveEvidence(evidence) {
const payload = toObject(evidence.payload);
if (String(payload?.source_layer ?? "").trim().toLowerCase() === "mcp_live_probe") {
return true;
}
const sourceEntity = String(evidence.pointer?.source?.entity ?? "").toLowerCase();
return sourceEntity.includes("mcplivemovement");
}
function extractEvidencePeriod(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 isExpectedAccountScopeMatch(accounts, expectedPrefixes) {
if (accounts.length === 0 || expectedPrefixes.length === 0) {
return true;
}
const prefixes = accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item));
if (prefixes.length === 0) {
return true;
}
return prefixes.some((prefix) => expectedPrefixes.includes(prefix));
}
function hasWrongDomainByAccounts(accounts, focusDomainHint) {
if (accounts.length === 0 || !focusDomainHint) {
return false;
}
const prefixes = accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item));
if (prefixes.length === 0) {
return false;
}
if (focusDomainHint === "settlements_60_62") {
return prefixes.every((prefix) => isVatPrefix(prefix));
}
if (focusDomainHint === "vat_document_register_book") {
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isMonthClosePrefix(prefix));
}
if (focusDomainHint === "month_close_costs_20_44") {
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix));
}
return false;
}
function extractLiveMatchedRows(result) {
const summary = toObject(result.summary);
const live = toObject(summary?.live_mcp);
const value = Number(live?.matched_rows);
return Number.isFinite(value) ? value : null;
}
function liveAccountScopeWasApplied(result) {
const summary = toObject(result.summary);
const live = toObject(summary?.live_mcp);
const accountScope = live?.account_scope;
return Array.isArray(accountScope) && accountScope.length > 0;
}
function evidenceAdmissibilityReasons(input) {
const reasons = new Set();
if (input.evidence.limitation?.reason_code === "weak_source_mapping") {
reasons.add("weak_source_mapping");
}
if (input.zeroLiveMatch && isLiveEvidence(input.evidence)) {
reasons.add("zero_live_match");
}
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 accounts = evidenceAccounts(input.evidence);
if (!isExpectedAccountScopeMatch(accounts, input.expectedPrefixes)) {
reasons.add("wrong_account_scope");
}
if (hasWrongDomainByAccounts(accounts, input.focusDomainHint)) {
reasons.add("wrong_domain");
}
return Array.from(reasons);
}
function isLiveItem(item) {
return String(item.source_layer ?? "").trim().toLowerCase() === "mcp_live_probe";
}
function itemPeriod(item) {
const value = String(item.period ?? item.Period ?? "").trim();
return value || null;
}
function itemAccounts(item) {
const direct = uniqueStrings([
...extractAccountsFromUnknown(item.account_context),
...extractAccountsFromUnknown(item.account_debit),
...extractAccountsFromUnknown(item.account_credit)
]);
if (direct.length > 0) {
return direct;
}
return uniqueStrings([...extractAccountsFromUnknown(item)]);
}
function itemRejectReasons(input) {
const reasons = new Set();
if (input.zeroLiveMatch && isLiveItem(input.item)) {
reasons.add("zero_live_match");
}
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 accounts = itemAccounts(input.item);
if (!isExpectedAccountScopeMatch(accounts, input.expectedPrefixes)) {
reasons.add("wrong_account_scope");
}
if (hasWrongDomainByAccounts(accounts, input.focusDomainHint)) {
reasons.add("wrong_domain");
}
return Array.from(reasons);
}
function addRejectReason(target, reason) {
target[reason] += 1;
}
function applyEvidenceAdmissibilityGate(input) {
const rejectBreakdown = initRejectBreakdown();
const categoryBreakdown = {
hard_evidence: 0,
supporting_signal: 0,
inadmissible_noise: 0
};
let candidateEvidenceTotal = 0;
let admissibleEvidenceCount = 0;
let rejectedEvidenceCount = 0;
let rejectedItemCount = 0;
const expectedPrefixes = expectedAccountPrefixes({
focusDomainHint: input.focusDomainHint,
polarity: input.polarity,
companyAnchors: input.companyAnchors,
userMessage: input.userMessage
});
const adjusted = input.retrievalResults.map((result) => {
const matchedRows = extractLiveMatchedRows(result);
const zeroLiveMatch = matchedRows === 0 && liveAccountScopeWasApplied(result);
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
candidateEvidenceTotal += evidence.length;
const admissibleEvidence = [];
for (const item of evidence) {
const reasons = evidenceAdmissibilityReasons({
evidence: item,
temporal: input.temporal,
focusDomainHint: input.focusDomainHint,
expectedPrefixes,
zeroLiveMatch
});
if (reasons.length > 0) {
rejectedEvidenceCount += 1;
categoryBreakdown.inadmissible_noise += 1;
for (const reason of reasons) {
addRejectReason(rejectBreakdown, reason);
}
continue;
}
const limitationCode = String(item.limitation?.reason_code ?? "").trim();
if (!limitationCode && item.confidence !== "low") {
categoryBreakdown.hard_evidence += 1;
}
else {
categoryBreakdown.supporting_signal += 1;
}
admissibleEvidenceCount += 1;
admissibleEvidence.push(item);
}
const items = Array.isArray(result.items) ? result.items : [];
const admissibleItems = [];
for (const item of items) {
const reasons = itemRejectReasons({
item,
temporal: input.temporal,
focusDomainHint: input.focusDomainHint,
expectedPrefixes,
zeroLiveMatch
});
if (reasons.length > 0) {
rejectedItemCount += 1;
for (const reason of reasons) {
addRejectReason(rejectBreakdown, reason);
}
continue;
}
admissibleItems.push(item);
}
const summary = {
...(toObject(result.summary) ?? {}),
evidence_admissibility_gate: {
candidate_evidence: evidence.length,
admissible_evidence: admissibleEvidence.length,
rejected_evidence: Math.max(0, evidence.length - admissibleEvidence.length),
rejected_items: Math.max(0, items.length - admissibleItems.length)
}
};
const limitations = [...(result.limitations ?? [])];
if (zeroLiveMatch) {
limitations.push("Live probe matched_rows=0; live evidence excluded from grounded answer.");
}
if (admissibleEvidence.length === 0 && evidence.length > 0) {
limitations.push("Admissibility gate removed non-admissible evidence for current scope.");
}
const normalizedStatus = result.status === "ok" && admissibleEvidence.length === 0 && admissibleItems.length === 0
? "partial"
: result.status;
return {
...result,
status: normalizedStatus,
items: admissibleItems,
evidence: admissibleEvidence,
summary,
limitations: uniqueStrings(limitations)
};
});
const reasonCodes = [];
if (rejectedEvidenceCount > 0) {
reasonCodes.push("inadmissible_evidence_filtered");
}
if (admissibleEvidenceCount === 0) {
reasonCodes.push("no_admissible_evidence_for_grounded_answer");
}
if (rejectedItemCount > 0) {
reasonCodes.push("inadmissible_items_filtered");
}
return {
retrievalResults: adjusted,
audit: {
candidate_evidence_total: candidateEvidenceTotal,
admissible_evidence_count: admissibleEvidenceCount,
rejected_evidence_count: rejectedEvidenceCount,
rejected_item_count: rejectedItemCount,
reject_breakdown: rejectBreakdown,
category_breakdown: categoryBreakdown,
reason_codes: uniqueStrings(reasonCodes)
}
};
}
function evaluateGroundedAnswerEligibility(input) {
const temporalPassed = input.temporal.temporal_guard_outcome === "passed";
const polarityPassed = !input.polarity.applied || input.polarity.outcome === "passed" || input.polarity.outcome === "not_applicable";
const admissibleEvidenceCount = input.evidence.admissible_evidence_count;
const criticalContradiction = Boolean(input.polarity.critical_contradiction);
const eligible = temporalPassed && polarityPassed && admissibleEvidenceCount > 0 && !criticalContradiction;
const reasonCodes = [];
if (!temporalPassed) {
reasonCodes.push(`temporal_guard_${input.temporal.temporal_guard_outcome}`);
}
if (!polarityPassed) {
reasonCodes.push(`polarity_guard_${input.polarity.outcome}`);
}
if (admissibleEvidenceCount <= 0) {
reasonCodes.push("admissible_evidence_count_zero");
}
if (criticalContradiction) {
reasonCodes.push("critical_domain_or_account_contradiction");
}
return {
eligible,
temporal_passed: temporalPassed,
polarity_passed: polarityPassed,
admissible_evidence_count: admissibleEvidenceCount,
critical_contradiction: criticalContradiction,
outcome: eligible ? "grounded_allowed" : "limited_or_insufficient_evidence",
reason_codes: uniqueStrings(reasonCodes)
};
}
function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
if (eligibility.eligible) {
return groundingCheck;
}
const status = eligibility.admissible_evidence_count <= 0 || !eligibility.temporal_passed ? "no_grounded_answer" : "partial";
const reasonMap = {
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-контуре."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
...eligibility.reason_codes.map((code) => reasonMap[code] ?? code)
];
return {
...groundingCheck,
status,
reasons: uniqueStrings(reasons)
};
}