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

1040 lines
41 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 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 = {
"\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) {
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);
}
const genericAccountPattern = /\b(\d{2}(?:\.\d{2})?)\b/g;
let genericMatch = 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);
}
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 shiftIsoDay(iso, deltaDays) {
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) {
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) {
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,
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 = "passed";
let normalizedAnchorDriftDetected = false;
if (normalizedAnchor.value && julyAnchor.window && !isPeriodWithinWindow(normalizedAnchor.value, julyAnchor.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,
temporal_resolution_source: julyAnchor.source,
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
};
}
function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
if (!temporal.temporal_guard_applied) {
return executionPlan;
}
const hint = temporal.primary_period_window?.granularity === "day" && temporal.resolved_time_anchor
? `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)) {
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|счет\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|счет\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"
? "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|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) {
return item;
}
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\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 evidenceContextExpansionMeta(evidence) {
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) {
const allowed = Boolean(item.context_expansion_allowed);
const reason = String(item.context_expansion_reason ?? "").trim() || null;
return { allowed, reason };
}
function withinAllowedContextWindow(normalizedPeriod, temporal) {
if (!temporal.allowed_context_window) {
return false;
}
return normalizedPeriod >= temporal.allowed_context_window.from && normalizedPeriod <= temporal.allowed_context_window.to;
}
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);
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);
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);
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);
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();
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;
}
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 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 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 = [];
if (!temporalPassed) {
reasonCodes.push(`temporal_guard_${input.temporal.temporal_guard_outcome}`);
}
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");
}
return {
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)
};
}
function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
if (eligibility.eligible) {
return groundingCheck;
}
const status = eligibility.admissible_evidence_count <= 0 || !eligibility.temporal_passed || !eligibility.claim_anchors_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-контуре.",
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 : []),
...eligibility.reason_codes.map((code) => reasonMap[code] ?? code)
];
return {
...groundingCheck,
status,
reasons: uniqueStrings(reasons)
};
}