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

1487 lines
59 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";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
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 iconv_lite_1 = __importDefault(require("iconv-lite"));
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 collectDateLikeSpans(text) {
const spans = [];
const patterns = [
/\b20\d{2}(?:[-/.](?:0?[1-9]|1[0-2]))(?:[-/.](?:0?[1-9]|[12]\d|3[01]))?\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+20\d{2})?\b/giu
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectAmountLikeSpans(text) {
const spans = [];
const patterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectPercentLikeSpans(text) {
const spans = [];
const pattern = /\b\d{1,3}(?:[.,]\d+)?\s*%/g;
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
return spans;
}
function collectContractLikeSpans(text) {
const spans = [];
const patterns = [
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function intersectsSpan(start, end, spans) {
return spans.some((span) => start < span.end && end > span.start);
}
function hasAccountContextAround(text, start, end) {
const left = text.slice(Math.max(0, start - 28), start);
const right = text.slice(end, Math.min(text.length, end + 28));
return /(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
}
function extractAccountsFromTextDetailed(text, options) {
const lower = String(text ?? "").toLowerCase();
const accounts = new Set();
const dateSpans = collectDateLikeSpans(lower);
const amountSpans = collectAmountLikeSpans(lower);
const percentSpans = collectPercentLikeSpans(lower);
const contractSpans = collectContractLikeSpans(lower);
const blockedSpans = [...dateSpans, ...amountSpans, ...percentSpans, ...contractSpans];
const hasAccountingLexeme = /(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(lower);
const contextualPattern = /(?:^|[^\p{L}\d])(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)[\s#:№]*((?:\d{2})(?:\.\d{2})?)/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const token = String(contextualMatch[1] ?? "").trim();
const prefix = token.match(/^(\d{2})/)?.[1] ?? null;
if (token && prefix && KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
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();
const leftPrefix = left.match(/^(\d{2})/)?.[1] ?? null;
const rightPrefix = right.match(/^(\d{2})/)?.[1] ?? null;
if (left && leftPrefix && KNOWN_ACCOUNT_PREFIXES.has(leftPrefix))
accounts.add(left);
if (right && rightPrefix && KNOWN_ACCOUNT_PREFIXES.has(rightPrefix))
accounts.add(right);
}
const genericAccountPattern = /\b(\d{2}(?:\.\d{2})?)\b/g;
let genericMatch = null;
const classifiedNumericTokens = [];
const rejectedAsNonAccounts = new Set();
while ((genericMatch = genericAccountPattern.exec(lower)) !== null) {
const token = String(genericMatch[1] ?? "").trim();
const start = genericMatch.index;
const end = start + token.length;
const prefix = token.match(/^(\d{2})/)?.[1] ?? null;
if (options?.forceAccountContext === true && prefix && KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
accounts.add(token);
classifiedNumericTokens.push({
token,
classification: "account_token"
});
continue;
}
if (intersectsSpan(start, end, dateSpans)) {
classifiedNumericTokens.push({
token,
classification: "date_token"
});
rejectedAsNonAccounts.add(token);
continue;
}
if (intersectsSpan(start, end, amountSpans)) {
classifiedNumericTokens.push({
token,
classification: "amount_token"
});
rejectedAsNonAccounts.add(token);
continue;
}
if (intersectsSpan(start, end, percentSpans)) {
classifiedNumericTokens.push({
token,
classification: "percent_token"
});
rejectedAsNonAccounts.add(token);
continue;
}
if (intersectsSpan(start, end, contractSpans)) {
classifiedNumericTokens.push({
token,
classification: "other_numeric"
});
rejectedAsNonAccounts.add(token);
continue;
}
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
classifiedNumericTokens.push({
token,
classification: "other_numeric"
});
rejectedAsNonAccounts.add(token);
continue;
}
if (!hasAccountingLexeme || !hasAccountContextAround(lower, start, end)) {
classifiedNumericTokens.push({
token,
classification: "other_numeric"
});
rejectedAsNonAccounts.add(token);
continue;
}
accounts.add(token);
classifiedNumericTokens.push({
token,
classification: "account_token"
});
}
const rawNumericTokens = uniqueStrings((lower.match(/\b\d{1,4}(?:[.,]\d{1,4})?\b/g) ?? []).map((item) => String(item)));
for (const token of accounts) {
if (!classifiedNumericTokens.some((item) => item.token === token && item.classification === "account_token")) {
classifiedNumericTokens.push({
token,
classification: "account_token"
});
}
}
// Numeric tokens hidden behind blocked spans still need explicit audit markers.
const blockedMatchPattern = /\b\d{2}(?:\.\d{2})?\b/g;
let blockedMatch = null;
while ((blockedMatch = blockedMatchPattern.exec(lower)) !== null) {
const token = String(blockedMatch[0] ?? "").trim();
const start = blockedMatch.index;
const end = start + token.length;
if (!intersectsSpan(start, end, blockedSpans)) {
continue;
}
if (classifiedNumericTokens.some((item) => item.token === token)) {
continue;
}
const classification = intersectsSpan(start, end, dateSpans)
? "date_token"
: intersectsSpan(start, end, amountSpans)
? "amount_token"
: "percent_token";
classifiedNumericTokens.push({
token,
classification
});
rejectedAsNonAccounts.add(token);
}
return {
resolved_account_anchors: Array.from(accounts),
raw_numeric_tokens: rawNumericTokens,
classified_numeric_tokens: classifiedNumericTokens,
rejected_as_non_accounts: Array.from(rejectedAsNonAccounts)
};
}
function extractAccountsFromText(text) {
return extractAccountsFromTextDetailed(text).resolved_account_anchors;
}
function extractAccountsFromUnknown(value, pathKey = "") {
if (Array.isArray(value)) {
return uniqueStrings(value.flatMap((item) => extractAccountsFromUnknown(item, pathKey)));
}
if (value && typeof value === "object") {
return uniqueStrings(Object.entries(value).flatMap(([key, item]) => extractAccountsFromUnknown(item, `${pathKey}.${String(key).toLowerCase()}`)));
}
if (typeof value !== "string" && typeof value !== "number") {
return [];
}
const contextPath = String(pathKey ?? "").toLowerCase();
if (contextPath.length > 0 &&
!/(?:account|счет|сч|debit|credit|дт|кт|konto|subkonto|analytics|context)/iu.test(contextPath)) {
return [];
}
const forceAccountContext = /(?:account|счет|сч|debit|credit|дт|кт|konto|subkonto|analytics|context)/iu.test(contextPath);
return extractAccountsFromTextDetailed(String(value), {
forceAccountContext
}).resolved_account_anchors;
}
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 inferPrimaryWindowFromAnchor(anchor) {
const raw = String(anchor ?? "").trim();
if (/^\d{4}-\d{2}$/.test(raw)) {
return {
from: `${raw}-01`,
to: `${raw}-31`,
granularity: "month"
};
}
const normalized = normalizeEvidenceDate(raw);
if (!normalized) {
return null;
}
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
return {
from: normalized,
to: normalized,
granularity: "day"
};
}
const month = normalized.slice(0, 7);
if (!/^\d{4}-\d{2}$/.test(month)) {
return null;
}
return {
from: `${month}-01`,
to: `${month}-31`,
granularity: "month"
};
}
function toTemporalGuardInput(window, fallback) {
if (window) {
return `${window.from}..${window.to}`;
}
const value = String(fallback ?? "").trim();
return value || null;
}
function normalizeIsoDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function normalizeTemporalWindow(input) {
const asOfDate = normalizeIsoDate(input.asOfDate);
if (asOfDate) {
return {
from: asOfDate,
to: asOfDate,
granularity: "day"
};
}
const from = normalizeIsoDate(input.periodFrom);
const to = normalizeIsoDate(input.periodTo);
if (!from || !to) {
return null;
}
return {
from,
to,
granularity: from === to ? "day" : "month"
};
}
function resolveTemporalGuard(input) {
const analysisWindow = normalizeTemporalWindow({
asOfDate: input.analysisContext?.as_of_date,
periodFrom: input.analysisContext?.period_from,
periodTo: input.analysisContext?.period_to
});
if (analysisWindow) {
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
return {
raw_time_anchor: analysisWindow.from,
raw_time_scope: guardInput,
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
resolved_primary_period: analysisWindow,
effective_primary_period: analysisWindow,
temporal_guard_input: guardInput,
temporal_alignment_status: "aligned",
temporal_resolution_source: source,
temporal_guard_basis: "raw_time_scope_unlocked",
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: ["analysis_context_applied"]
};
}
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
const reasonCodes = [];
if (!julyAnchor.applyGuard) {
const resolvedWindow = inferPrimaryWindowFromAnchor(normalizedAnchor.value);
const guardInput = toTemporalGuardInput(resolvedWindow, normalizedAnchor.value);
return {
raw_time_anchor: julyAnchor.raw,
raw_time_scope: normalizedAnchor.value,
resolved_time_anchor: normalizedAnchor.value,
resolved_primary_period: resolvedWindow,
effective_primary_period: resolvedWindow,
temporal_guard_input: guardInput,
temporal_alignment_status: normalizedAnchor.value ? "aligned" : "conflicting",
temporal_resolution_source: normalizedAnchor.source,
temporal_guard_basis: normalizedAnchor.value ? "raw_time_scope_unlocked" : "none",
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: normalizedAnchor.value ? [] : ["missing_resolved_primary_period"]
};
}
let outcome = "passed";
let normalizedAnchorDriftDetected = false;
let temporalAlignmentStatus = "aligned";
if (normalizedAnchor.value && julyAnchor.window && !isPeriodWithinWindow(normalizedAnchor.value, julyAnchor.window)) {
normalizedAnchorDriftDetected = true;
temporalAlignmentStatus = "corrected";
reasonCodes.push("normalized_anchor_out_of_primary_window_overridden");
}
else if (!normalizedAnchor.value && !julyAnchor.resolved) {
outcome = "ambiguous_limited";
temporalAlignmentStatus = "conflicting";
reasonCodes.push("missing_time_anchor_under_snapshot_lock");
}
const allowedContextWindow = buildAllowedContextWindow(julyAnchor.window);
const resolvedPrimaryPeriod = julyAnchor.window;
const effectivePrimaryPeriod = resolvedPrimaryPeriod ?? inferPrimaryWindowFromAnchor(julyAnchor.resolved ?? normalizedAnchor.value);
const guardInput = toTemporalGuardInput(effectivePrimaryPeriod, julyAnchor.resolved ?? normalizedAnchor.value);
return {
raw_time_anchor: julyAnchor.raw,
raw_time_scope: normalizedAnchor.value,
resolved_time_anchor: julyAnchor.resolved ?? normalizedAnchor.value,
resolved_primary_period: resolvedPrimaryPeriod,
effective_primary_period: effectivePrimaryPeriod,
temporal_guard_input: guardInput,
temporal_alignment_status: temporalAlignmentStatus,
temporal_resolution_source: julyAnchor.source,
temporal_guard_basis: julyAnchor.window ? "resolved_primary_period" : "none",
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 primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
const periodLabel = primaryWindow
? `${primaryWindow.from}..${primaryWindow.to}`
: temporal.resolved_time_anchor
? temporal.resolved_time_anchor
: "active_period";
const hint = primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
: `primary period ${periodLabel}; 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 mojibakeScoreForRuntimeGuards(value) {
const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const hardMarkers = (source.match(/[ѓ“‚„…†‡€‰‹‰ЉЊ‹Џ‘’“”•–—™љ›њћџ]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Гђ.|Г‘.)/g) ?? []).length;
const doubleEncodedMarkers = (source.match(/(?:Р“.|Р.|Гѓ.|Г‚.)/gu) ?? []).length;
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
}
function looksLikeMojibakeForRuntimeGuards(value) {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[ѓ“‚„…†‡€‰‹‰ЉЊ‹Џ‘’“”•–—™љ›њћџ]/.test(source)) {
return true;
}
if ((source.match(/(?:Р.|С.|Гђ.|Г‘.)/g) ?? []).length >= 2) {
return true;
}
return (source.match(/(?:Р“.|Р.|Гѓ.|Г‚.)/gu) ?? []).length >= 2;
}
function repairRuntimeGuardsMojibake(value) {
const source = String(value ?? "");
if (!looksLikeMojibakeForRuntimeGuards(source)) {
return source;
}
let candidate = source;
for (let pass = 0; pass < 3; pass += 1) {
let improved = false;
try {
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
if (mojibakeScoreForRuntimeGuards(fromWin1251) > mojibakeScoreForRuntimeGuards(candidate)) {
candidate = fromWin1251;
improved = true;
}
}
catch (_error) {
// noop
}
try {
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
if (mojibakeScoreForRuntimeGuards(fromLatin1) > mojibakeScoreForRuntimeGuards(candidate)) {
candidate = fromLatin1;
improved = true;
}
}
catch (_error) {
// noop
}
if (!improved) {
break;
}
}
return candidate;
}
function resolveDomainPolarityGuard(input) {
const repairedMessage = repairRuntimeGuardsMojibake(String(input.userMessage ?? ""));
const lower = repairedMessage.toLowerCase();
const accountExtraction = extractAccountsFromTextDetailed(lower);
let accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...accountExtraction.resolved_account_anchors]);
if (accounts.length === 0) {
const directAccountMentions = Array.from(repairedMessage.matchAll(/(?:^|[^\p{L}\d])(?:сч(?:е|ё)т(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)[\s#:№]*((?:60|62|76)(?:\.\d{2})?)/giu))
.map((match) => String(match[1] ?? "").trim())
.filter((token) => token.length > 0);
if (directAccountMentions.length > 0) {
accounts = uniqueStrings([...accounts, ...directAccountMentions]);
}
}
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,
raw_numeric_tokens: accountExtraction.raw_numeric_tokens,
classified_numeric_tokens: accountExtraction.classified_numeric_tokens,
rejected_as_non_accounts: accountExtraction.rejected_as_non_accounts,
resolved_account_anchors: 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)/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)/i.test(lower) ? 1 : 0);
let polarity = "mixed_or_unresolved";
if (supplierScore > 0 || customerScore > 0) {
if (supplierScore >= customerScore + 2) {
polarity = "supplier_payable";
}
else if (customerScore >= supplierScore + 2) {
polarity = "customer_receivable";
}
else if (prefixes.has("60") && !prefixes.has("62")) {
polarity = "supplier_payable";
}
else if (prefixes.has("62") && !prefixes.has("60")) {
polarity = "customer_receivable";
}
}
const unresolved = polarity === "mixed_or_unresolved";
const reasonCodes = unresolved ? ["unresolved_supplier_customer_polarity"] : [];
if (unresolved && supplierScore > 0 && customerScore > 0) {
reasonCodes.push("supplier_customer_signals_conflict");
}
if (unresolved && supplierScore === 0 && customerScore === 0) {
reasonCodes.push("supplier_customer_signals_absent");
}
return {
applied: true,
polarity,
outcome: unresolved ? "limited_unresolved_polarity" : "passed",
supplier_score: supplierScore,
customer_score: customerScore,
account_scope: accounts,
raw_numeric_tokens: accountExtraction.raw_numeric_tokens,
classified_numeric_tokens: accountExtraction.classified_numeric_tokens,
rejected_as_non_accounts: accountExtraction.rejected_as_non_accounts,
resolved_account_anchors: accounts,
rejected_problem_units: 0,
rejected_evidence: 0,
critical_contradiction: unresolved,
reason_codes: uniqueStrings(reasonCodes)
};
}
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)/i.test(text)) {
return item;
}
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\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 isFixedAssetPrefix(prefix) {
return prefix === "01" || prefix === "02" || prefix === "08";
}
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 === "fixed_asset_amortization") {
return ["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));
}
if (focusDomainHint === "fixed_asset_amortization") {
const hasFixedAsset = prefixes.some((prefix) => isFixedAssetPrefix(prefix));
if (hasFixedAsset) {
return false;
}
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix) || isMonthClosePrefix(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 effectivePrimaryPeriodWindow(temporal) {
return temporal.effective_primary_period ?? temporal.primary_period_window;
}
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);
const primaryWindow = effectivePrimaryPeriodWindow(input.temporal);
if (period && primaryWindow) {
const normalized = normalizeEvidenceDate(period);
const expansionMeta = evidenceContextExpansionMeta(input.evidence);
if (normalized && !isPeriodWithinWindow(normalized, primaryWindow)) {
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 > primaryWindow.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);
const primaryWindow = effectivePrimaryPeriodWindow(input.temporal);
if (period && primaryWindow) {
const normalized = normalizeEvidenceDate(period);
const expansionMeta = itemContextExpansionMeta(input.item);
if (normalized && !isPeriodWithinWindow(normalized, primaryWindow)) {
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 > primaryWindow.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 eligibilityTimeBasis = input.temporal.temporal_guard_basis;
const scopeValues = Array.isArray(input.businessScopeResolved) ? input.businessScopeResolved : [];
const hasCompanyScope = scopeValues.includes("company_specific_accounting");
const hasOnlyGenericScope = scopeValues.length > 0 && scopeValues.every((item) => String(item ?? "").trim() === "generic_accounting");
const businessScopePassed = scopeValues.length === 0 ? true : hasCompanyScope || !hasOnlyGenericScope;
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 &&
businessScopePassed &&
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 (!businessScopePassed) {
reasonCodes.push("business_scope_generic_unresolved");
}
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,
eligibility_time_basis: eligibilityTimeBasis,
business_scope_passed: businessScopePassed,
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 ||
!eligibility.business_scope_passed
? "no_grounded_answer"
: "partial";
const reasonMap = {
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных. Temporal anchor outside snapshot window.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
...eligibility.reason_codes.map((code) => reasonMap[code] ?? code)
];
return {
...groundingCheck,
status,
reasons: uniqueStrings(reasons)
};
}