1418 lines
56 KiB
JavaScript
1418 lines
56 KiB
JavaScript
"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 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 = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расч[её]т|расчет|аванс|долг|settlement|payment|supplier|customer|постав|покуп)/iu.test(lower);
|
||
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b\s*(?:№|#|:)?\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 resolveDomainPolarityGuard(input) {
|
||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||
const accountExtraction = extractAccountsFromTextDetailed(lower);
|
||
const accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...accountExtraction.resolved_account_anchors]);
|
||
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_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)
|
||
};
|
||
}
|