Этап 4 / Волна 18 закрытие блокеров по времени, доменной полярности и допуску доказательной базы

This commit is contained in:
2026-03-29 00:40:06 +03:00
parent 7eb1410501
commit d7e145010b
140 changed files with 417053 additions and 42196 deletions
@@ -0,0 +1,887 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveTemporalGuard = resolveTemporalGuard;
exports.applyTemporalHintToExecutionPlan = applyTemporalHintToExecutionPlan;
exports.resolveDomainPolarityGuard = resolveDomainPolarityGuard;
exports.applyPolarityHintToExecutionPlan = applyPolarityHintToExecutionPlan;
exports.applyDomainPolarityGuardToRetrievalResults = applyDomainPolarityGuardToRetrievalResults;
exports.applyEvidenceAdmissibilityGate = applyEvidenceAdmissibilityGate;
exports.evaluateGroundedAnswerEligibility = evaluateGroundedAnswerEligibility;
exports.applyEligibilityToGroundingCheck = applyEligibilityToGroundingCheck;
const JULY_YEAR = "2020";
const JULY_MONTH = "07";
const JULY_WINDOW = {
from: "2020-07-01",
to: "2020-07-31",
granularity: "month"
};
const RUS_MONTH_TO_NUMBER = {
января: "01",
январь: "01",
февраля: "02",
февраль: "02",
марта: "03",
март: "03",
апреля: "04",
апрель: "04",
мая: "05",
май: "05",
июня: "06",
июнь: "06",
июля: "07",
июль: "07",
августа: "08",
август: "08",
сентября: "09",
сентябрь: "09",
октября: "10",
октябрь: "10",
ноября: "11",
ноябрь: "11",
декабря: "12",
декабрь: "12"
};
function uniqueStrings(values) {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean)));
}
function toObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toObjectArray(value) {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item) => Boolean(item) && typeof item === "object");
}
function accountPrefix(value) {
const token = String(value ?? "").trim();
const match = token.match(/^(\d{2})/);
return match ? match[1] : null;
}
function extractAccountsFromText(text) {
const lower = String(text ?? "").toLowerCase();
const accounts = new Set();
const contextualPattern = /(?:\b(?:сч(?:е|ё)т(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const token = String(contextualMatch[1] ?? "").trim();
if (token) {
accounts.add(token);
}
}
const pairPattern = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/g;
let pairMatch = null;
while ((pairMatch = pairPattern.exec(lower)) !== null) {
const left = String(pairMatch[1] ?? "").trim();
const right = String(pairMatch[2] ?? "").trim();
if (left)
accounts.add(left);
if (right)
accounts.add(right);
}
return Array.from(accounts);
}
function extractAccountsFromUnknown(value) {
if (Array.isArray(value)) {
return uniqueStrings(value.flatMap((item) => extractAccountsFromUnknown(item)));
}
if (value && typeof value === "object") {
return uniqueStrings(Object.values(value).flatMap((item) => extractAccountsFromUnknown(item)));
}
if (typeof value !== "string" && typeof value !== "number") {
return [];
}
const text = String(value);
const matches = text.match(/\b\d{2}(?:\.\d{2})?\b/g) ?? [];
return uniqueStrings(matches);
}
function normalizeTwoDigits(value) {
return String(value).padStart(2, "0");
}
function parseYear(raw) {
const token = String(raw ?? "").trim();
if (token.length === 2) {
return `20${token}`;
}
return token;
}
function normalizeDateIso(input) {
const year = String(input.year ?? "").trim();
const month = normalizeTwoDigits(input.month ?? "");
if (!/^\d{4}$/.test(year) || !/^\d{2}$/.test(month)) {
return null;
}
if (input.day === undefined || input.day === null || String(input.day).trim().length === 0) {
return `${year}-${month}`;
}
const day = normalizeTwoDigits(input.day ?? "");
if (!/^\d{2}$/.test(day)) {
return null;
}
return `${year}-${month}-${day}`;
}
function parseDateLike(raw) {
const value = String(raw ?? "").trim().toLowerCase();
if (!value) {
return null;
}
const isoDay = value.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01])\b/);
if (isoDay) {
return normalizeDateIso({ year: isoDay[1], month: isoDay[2], day: isoDay[3] });
}
const isoMonth = value.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
if (isoMonth) {
return normalizeDateIso({ year: isoMonth[1], month: isoMonth[2] });
}
const dayMonthYear = value.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?[1-9]|1[0-2])[./-](\d{2}|\d{4})\b/);
if (dayMonthYear) {
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
}
const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i);
if (rusMonthYear) {
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
if (!month)
return null;
return normalizeDateIso({ year: rusMonthYear[2], month });
}
return null;
}
function monthStart(iso) {
const month = String(iso ?? "").slice(0, 7);
if (!/^\d{4}-\d{2}$/.test(month)) {
return null;
}
return `${month}-01`;
}
function normalizeEvidenceDate(value) {
const parsed = parseDateLike(value);
if (!parsed) {
return null;
}
if (/^\d{4}-\d{2}-\d{2}$/.test(parsed)) {
return parsed;
}
if (/^\d{4}-\d{2}$/.test(parsed)) {
return monthStart(parsed);
}
return null;
}
function isPeriodWithinWindow(periodIso, window) {
const normalized = normalizeEvidenceDate(periodIso);
if (!normalized) {
return false;
}
return normalized >= window.from && normalized <= window.to;
}
function extractNormalizedFragments(normalized) {
if (!normalized || typeof normalized !== "object") {
return [];
}
const source = normalized;
return toObjectArray(source.fragments);
}
function normalizedAnchorFromFragments(normalized) {
const fragments = extractNormalizedFragments(normalized);
for (const fragment of fragments) {
const timeScope = toObject(fragment.time_scope);
const type = String(timeScope?.type ?? "").trim().toLowerCase();
const value = String(timeScope?.value ?? "").trim();
if (!value) {
continue;
}
const parsed = parseDateLike(value);
if (parsed) {
return {
value: parsed,
source: `normalized_time_scope:${type || "unknown"}`
};
}
if (/(?:июл|july)/i.test(value)) {
return {
value: `${JULY_YEAR}-${JULY_MONTH}`,
source: `normalized_time_scope:${type || "unknown"}`
};
}
}
return {
value: null,
source: "normalized_time_scope:missing"
};
}
function collectRawTemporalAnchorText(userMessage, companyAnchors) {
return [userMessage, ...(companyAnchors?.periods ?? []), ...(companyAnchors?.dates ?? [])]
.map((item) => String(item ?? "").trim())
.filter(Boolean)
.join(" ");
}
function resolveJulyAnchor(rawText) {
const raw = String(rawText ?? "");
const lower = raw.toLowerCase();
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
const monthByNamed = /(июл|july)/i.test(lower);
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
return {
raw: null,
resolved: null,
source: "no_july_anchor",
window: null,
applyGuard: false
};
}
const dayValue = dayByNamedJuly?.[1] ?? dayByNumeric?.[1] ?? null;
const explicitDayYear = dayByNumeric?.[3] ? parseYear(dayByNumeric[3]) : null;
const anchorYear = explicitDayYear ?? explicitYear ?? JULY_YEAR;
const applyGuard = anchorYear === JULY_YEAR;
if (!applyGuard) {
return {
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? (monthByNamed ? "июль" : "07"),
resolved: normalizeDateIso({
year: anchorYear,
month: JULY_MONTH,
...(dayValue ? { day: dayValue } : {})
}),
source: "explicit_non_snapshot_year",
window: null,
applyGuard: false
};
}
if (dayValue) {
const dayIso = normalizeDateIso({
year: JULY_YEAR,
month: JULY_MONTH,
day: dayValue
});
if (dayIso) {
return {
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? null,
resolved: dayIso,
source: "company_snapshot_july_day_lock",
window: {
from: dayIso,
to: dayIso,
granularity: "day"
},
applyGuard: true
};
}
}
return {
raw: monthByNamed ? "июль" : "2020-07",
resolved: `${JULY_YEAR}-${JULY_MONTH}`,
source: "company_snapshot_july_month_lock",
window: JULY_WINDOW,
applyGuard: true
};
}
function resolveTemporalGuard(input) {
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
const reasonCodes = [];
if (!julyAnchor.applyGuard) {
return {
raw_time_anchor: julyAnchor.raw,
resolved_time_anchor: normalizedAnchor.value,
temporal_resolution_source: normalizedAnchor.source,
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
reason_codes: []
};
}
let outcome = "passed";
if (normalizedAnchor.value && julyAnchor.window && !isPeriodWithinWindow(normalizedAnchor.value, julyAnchor.window)) {
outcome = "failed_out_of_snapshot_window";
reasonCodes.push("normalized_anchor_out_of_snapshot_window");
}
else if (!normalizedAnchor.value && !julyAnchor.resolved) {
outcome = "ambiguous_limited";
reasonCodes.push("missing_time_anchor_under_snapshot_lock");
}
return {
raw_time_anchor: julyAnchor.raw,
resolved_time_anchor: julyAnchor.resolved ?? normalizedAnchor.value,
temporal_resolution_source: julyAnchor.source,
temporal_guard_applied: true,
temporal_guard_outcome: outcome,
primary_period_window: julyAnchor.window,
reason_codes: reasonCodes
};
}
function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
if (!temporal.temporal_guard_applied) {
return executionPlan;
}
const hint = temporal.primary_period_window?.granularity === "day" && temporal.resolved_time_anchor
? `в рамках company snapshot даты ${temporal.resolved_time_anchor}`
: `в рамках company snapshot июля 2020 (${JULY_WINDOW.from}..${JULY_WINDOW.to})`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (/2020-07|июл|july/i.test(text)) {
return item;
}
return {
...item,
fragment_text: `${text}; ${hint}`.trim()
};
});
}
function resolveDomainPolarityGuard(input) {
const lower = String(input.userMessage ?? "").toLowerCase();
const accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(lower)]);
const prefixes = new Set(accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item)));
const settlementSignal = input.focusDomainHint === "settlements_60_62" ||
prefixes.has("60") ||
prefixes.has("62") ||
prefixes.has("51") ||
prefixes.has("76") ||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т)/i.test(lower);
if (!settlementSignal) {
return {
applied: false,
polarity: "not_applicable",
outcome: "not_applicable",
supplier_score: 0,
customer_score: 0,
account_scope: accounts,
rejected_problem_units: 0,
rejected_evidence: 0,
critical_contradiction: false,
reason_codes: []
};
}
const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable)/i.test(lower) ? 2 : 0) +
(prefixes.has("60") ? 2 : 0) +
(/(?:счет\s*60|по\s*60)/i.test(lower) ? 1 : 0);
const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable)/i.test(lower) ? 2 : 0) +
(prefixes.has("62") ? 2 : 0) +
(/(?:счет\s*62|по\s*62)/i.test(lower) ? 1 : 0);
let polarity = "mixed_or_unresolved";
if (supplierScore > 0 && customerScore === 0) {
polarity = "supplier_payable";
}
else if (customerScore > 0 && supplierScore === 0) {
polarity = "customer_receivable";
}
const unresolved = polarity === "mixed_or_unresolved";
return {
applied: true,
polarity,
outcome: unresolved ? "limited_unresolved_polarity" : "passed",
supplier_score: supplierScore,
customer_score: customerScore,
account_scope: accounts,
rejected_problem_units: 0,
rejected_evidence: 0,
critical_contradiction: unresolved,
reason_codes: unresolved ? ["unresolved_supplier_customer_polarity"] : []
};
}
function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
if (!polarity.applied || polarity.polarity === "mixed_or_unresolved" || polarity.polarity === "not_applicable") {
return executionPlan;
}
const hint = polarity.polarity === "supplier_payable"
? "контекст: расчеты с поставщиком, обязательство, счет 60"
: "контекст: расчеты с покупателем, дебиторская задолженность, счет 62";
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
}
const text = String(item.fragment_text ?? "").trim();
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|счет\s*60|по\s*60)/i.test(text)) {
return item;
}
if (polarity.polarity === "customer_receivable" && /(покупат|customer|счет\s*62|по\s*62)/i.test(text)) {
return item;
}
return {
...item,
fragment_text: `${text}; ${hint}`.trim()
};
});
}
function containsReceivableSignal(value) {
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
}
function containsPayableSignal(value) {
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
}
function problemUnitCorpus(unit) {
return [
unit.lifecycle_domain ?? "",
unit.problem_unit_type,
unit.business_defect_class ?? "",
unit.failed_expected_edge ?? "",
unit.invalid_transition ?? "",
unit.mechanism_summary ?? "",
unit.business_lifecycle_interpretation ?? "",
...(unit.affected_accounts ?? [])
]
.join(" ")
.toLowerCase();
}
function isProblemUnitCompatible(unit, polarity) {
if (polarity === "supplier_payable") {
return !containsReceivableSignal(problemUnitCorpus(unit));
}
if (polarity === "customer_receivable") {
return !containsPayableSignal(problemUnitCorpus(unit));
}
return true;
}
function evidenceCorpus(evidence) {
return JSON.stringify({
limitation: evidence.limitation,
payload: evidence.payload,
mechanism: evidence.mechanism_note,
source_ref: evidence.source_ref,
pointer: evidence.pointer
}).toLowerCase();
}
function evidenceAccounts(evidence) {
const payload = toObject(evidence.payload);
const direct = uniqueStrings([
...extractAccountsFromUnknown(payload?.account_context),
...extractAccountsFromUnknown(payload?.account_debit),
...extractAccountsFromUnknown(payload?.account_credit)
]);
if (direct.length > 0) {
return direct;
}
return uniqueStrings([
...extractAccountsFromUnknown(evidence.payload),
...extractAccountsFromUnknown(evidence.pointer ?? null)
]);
}
function isEvidenceCompatibleWithPolarity(evidence, polarity) {
const corpus = evidenceCorpus(evidence);
const accounts = evidenceAccounts(evidence).map((item) => accountPrefix(item)).filter((item) => Boolean(item));
if (polarity === "supplier_payable") {
if (containsReceivableSignal(corpus)) {
return false;
}
if (accounts.length > 0 && accounts.every((item) => item === "62")) {
return false;
}
}
if (polarity === "customer_receivable") {
if (containsPayableSignal(corpus)) {
return false;
}
if (accounts.length > 0 && accounts.every((item) => item === "60")) {
return false;
}
}
return true;
}
function applyDomainPolarityGuardToRetrievalResults(input) {
if (!input.guard.applied || input.guard.polarity === "not_applicable" || input.guard.polarity === "mixed_or_unresolved") {
return {
retrievalResults: input.retrievalResults,
audit: input.guard
};
}
let rejectedProblemUnits = 0;
let rejectedEvidence = 0;
let criticalContradiction = false;
const adjusted = input.retrievalResults.map((result) => {
const originalUnits = Array.isArray(result.problem_units) ? result.problem_units : [];
const filteredUnits = originalUnits.filter((unit) => isProblemUnitCompatible(unit, input.guard.polarity));
rejectedProblemUnits += Math.max(0, originalUnits.length - filteredUnits.length);
const originalEvidence = Array.isArray(result.evidence) ? result.evidence : [];
const filteredEvidence = originalEvidence.filter((item) => isEvidenceCompatibleWithPolarity(item, input.guard.polarity));
rejectedEvidence += Math.max(0, originalEvidence.length - filteredEvidence.length);
if (originalUnits.length > 0 && filteredUnits.length === 0 && originalEvidence.length > 0 && filteredEvidence.length === 0) {
criticalContradiction = true;
}
return {
...result,
evidence: filteredEvidence,
...(Array.isArray(result.problem_units)
? {
problem_units: filteredUnits
}
: {})
};
});
const reasonCodes = [];
if (rejectedProblemUnits > 0) {
reasonCodes.push("polarity_problem_unit_filter_applied");
}
if (rejectedEvidence > 0) {
reasonCodes.push("polarity_evidence_filter_applied");
}
if (criticalContradiction) {
reasonCodes.push("critical_domain_polarity_contradiction");
}
return {
retrievalResults: adjusted,
audit: {
...input.guard,
rejected_problem_units: rejectedProblemUnits,
rejected_evidence: rejectedEvidence,
critical_contradiction: criticalContradiction,
outcome: criticalContradiction ? "blocked_conflict" : "passed",
reason_codes: uniqueStrings([...(input.guard.reason_codes ?? []), ...reasonCodes])
}
};
}
function initRejectBreakdown() {
return {
wrong_period: 0,
wrong_domain: 0,
wrong_account_scope: 0,
weak_source_mapping: 0,
zero_live_match: 0,
future_dated_or_out_of_window: 0
};
}
function isVatPrefix(prefix) {
return prefix === "19" || prefix === "68";
}
function isSettlementPrefix(prefix) {
return prefix === "51" || prefix === "60" || prefix === "62" || prefix === "76";
}
function isMonthClosePrefix(prefix) {
const numeric = Number(prefix);
if (prefix === "97") {
return true;
}
if (!Number.isFinite(numeric)) {
return false;
}
return numeric >= 20 && numeric <= 44;
}
function expectedAccountPrefixes(input) {
const explicit = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(input.userMessage)])
.map((item) => accountPrefix(item))
.filter((item) => Boolean(item));
if (explicit.length > 0) {
return uniqueStrings(explicit);
}
if (input.focusDomainHint === "vat_document_register_book") {
return ["19", "68"];
}
if (input.focusDomainHint === "month_close_costs_20_44") {
return ["20", "25", "26", "44", "97", "01", "02", "08"];
}
if (input.focusDomainHint === "settlements_60_62") {
if (input.polarity === "supplier_payable") {
return ["60", "51", "76"];
}
if (input.polarity === "customer_receivable") {
return ["62", "51"];
}
return ["60", "62", "51", "76"];
}
return [];
}
function isLiveEvidence(evidence) {
const payload = toObject(evidence.payload);
if (String(payload?.source_layer ?? "").trim().toLowerCase() === "mcp_live_probe") {
return true;
}
const sourceEntity = String(evidence.pointer?.source?.entity ?? "").toLowerCase();
return sourceEntity.includes("mcplivemovement");
}
function extractEvidencePeriod(evidence) {
const payload = toObject(evidence.payload);
return (String(evidence.source_ref?.period ?? "").trim() ||
String(evidence.pointer?.source?.period ?? "").trim() ||
String(payload?.period ?? "").trim() ||
null);
}
function isExpectedAccountScopeMatch(accounts, expectedPrefixes) {
if (accounts.length === 0 || expectedPrefixes.length === 0) {
return true;
}
const prefixes = accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item));
if (prefixes.length === 0) {
return true;
}
return prefixes.some((prefix) => expectedPrefixes.includes(prefix));
}
function hasWrongDomainByAccounts(accounts, focusDomainHint) {
if (accounts.length === 0 || !focusDomainHint) {
return false;
}
const prefixes = accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item));
if (prefixes.length === 0) {
return false;
}
if (focusDomainHint === "settlements_60_62") {
return prefixes.every((prefix) => isVatPrefix(prefix));
}
if (focusDomainHint === "vat_document_register_book") {
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isMonthClosePrefix(prefix));
}
if (focusDomainHint === "month_close_costs_20_44") {
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix));
}
return false;
}
function extractLiveMatchedRows(result) {
const summary = toObject(result.summary);
const live = toObject(summary?.live_mcp);
const value = Number(live?.matched_rows);
return Number.isFinite(value) ? value : null;
}
function liveAccountScopeWasApplied(result) {
const summary = toObject(result.summary);
const live = toObject(summary?.live_mcp);
const accountScope = live?.account_scope;
return Array.isArray(accountScope) && accountScope.length > 0;
}
function evidenceAdmissibilityReasons(input) {
const reasons = new Set();
if (input.evidence.limitation?.reason_code === "weak_source_mapping") {
reasons.add("weak_source_mapping");
}
if (input.zeroLiveMatch && isLiveEvidence(input.evidence)) {
reasons.add("zero_live_match");
}
const period = extractEvidencePeriod(input.evidence);
if (period && input.temporal.primary_period_window) {
const normalized = normalizeEvidenceDate(period);
if (normalized && normalized > input.temporal.primary_period_window.to) {
reasons.add("future_dated_or_out_of_window");
}
else if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
reasons.add("wrong_period");
}
}
const accounts = evidenceAccounts(input.evidence);
if (!isExpectedAccountScopeMatch(accounts, input.expectedPrefixes)) {
reasons.add("wrong_account_scope");
}
if (hasWrongDomainByAccounts(accounts, input.focusDomainHint)) {
reasons.add("wrong_domain");
}
return Array.from(reasons);
}
function isLiveItem(item) {
return String(item.source_layer ?? "").trim().toLowerCase() === "mcp_live_probe";
}
function itemPeriod(item) {
const value = String(item.period ?? item.Period ?? "").trim();
return value || null;
}
function itemAccounts(item) {
const direct = uniqueStrings([
...extractAccountsFromUnknown(item.account_context),
...extractAccountsFromUnknown(item.account_debit),
...extractAccountsFromUnknown(item.account_credit)
]);
if (direct.length > 0) {
return direct;
}
return uniqueStrings([...extractAccountsFromUnknown(item)]);
}
function itemRejectReasons(input) {
const reasons = new Set();
if (input.zeroLiveMatch && isLiveItem(input.item)) {
reasons.add("zero_live_match");
}
const period = itemPeriod(input.item);
if (period && input.temporal.primary_period_window) {
const normalized = normalizeEvidenceDate(period);
if (normalized && normalized > input.temporal.primary_period_window.to) {
reasons.add("future_dated_or_out_of_window");
}
else if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
reasons.add("wrong_period");
}
}
const accounts = itemAccounts(input.item);
if (!isExpectedAccountScopeMatch(accounts, input.expectedPrefixes)) {
reasons.add("wrong_account_scope");
}
if (hasWrongDomainByAccounts(accounts, input.focusDomainHint)) {
reasons.add("wrong_domain");
}
return Array.from(reasons);
}
function addRejectReason(target, reason) {
target[reason] += 1;
}
function applyEvidenceAdmissibilityGate(input) {
const rejectBreakdown = initRejectBreakdown();
const categoryBreakdown = {
hard_evidence: 0,
supporting_signal: 0,
inadmissible_noise: 0
};
let candidateEvidenceTotal = 0;
let admissibleEvidenceCount = 0;
let rejectedEvidenceCount = 0;
let rejectedItemCount = 0;
const expectedPrefixes = expectedAccountPrefixes({
focusDomainHint: input.focusDomainHint,
polarity: input.polarity,
companyAnchors: input.companyAnchors,
userMessage: input.userMessage
});
const adjusted = input.retrievalResults.map((result) => {
const matchedRows = extractLiveMatchedRows(result);
const zeroLiveMatch = matchedRows === 0 && liveAccountScopeWasApplied(result);
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
candidateEvidenceTotal += evidence.length;
const admissibleEvidence = [];
for (const item of evidence) {
const reasons = evidenceAdmissibilityReasons({
evidence: item,
temporal: input.temporal,
focusDomainHint: input.focusDomainHint,
expectedPrefixes,
zeroLiveMatch
});
if (reasons.length > 0) {
rejectedEvidenceCount += 1;
categoryBreakdown.inadmissible_noise += 1;
for (const reason of reasons) {
addRejectReason(rejectBreakdown, reason);
}
continue;
}
const limitationCode = String(item.limitation?.reason_code ?? "").trim();
if (!limitationCode && item.confidence !== "low") {
categoryBreakdown.hard_evidence += 1;
}
else {
categoryBreakdown.supporting_signal += 1;
}
admissibleEvidenceCount += 1;
admissibleEvidence.push(item);
}
const items = Array.isArray(result.items) ? result.items : [];
const admissibleItems = [];
for (const item of items) {
const reasons = itemRejectReasons({
item,
temporal: input.temporal,
focusDomainHint: input.focusDomainHint,
expectedPrefixes,
zeroLiveMatch
});
if (reasons.length > 0) {
rejectedItemCount += 1;
for (const reason of reasons) {
addRejectReason(rejectBreakdown, reason);
}
continue;
}
admissibleItems.push(item);
}
const summary = {
...(toObject(result.summary) ?? {}),
evidence_admissibility_gate: {
candidate_evidence: evidence.length,
admissible_evidence: admissibleEvidence.length,
rejected_evidence: Math.max(0, evidence.length - admissibleEvidence.length),
rejected_items: Math.max(0, items.length - admissibleItems.length)
}
};
const limitations = [...(result.limitations ?? [])];
if (zeroLiveMatch) {
limitations.push("Live probe matched_rows=0; live evidence excluded from grounded answer.");
}
if (admissibleEvidence.length === 0 && evidence.length > 0) {
limitations.push("Admissibility gate removed non-admissible evidence for current scope.");
}
const normalizedStatus = result.status === "ok" && admissibleEvidence.length === 0 && admissibleItems.length === 0
? "partial"
: result.status;
return {
...result,
status: normalizedStatus,
items: admissibleItems,
evidence: admissibleEvidence,
summary,
limitations: uniqueStrings(limitations)
};
});
const reasonCodes = [];
if (rejectedEvidenceCount > 0) {
reasonCodes.push("inadmissible_evidence_filtered");
}
if (admissibleEvidenceCount === 0) {
reasonCodes.push("no_admissible_evidence_for_grounded_answer");
}
if (rejectedItemCount > 0) {
reasonCodes.push("inadmissible_items_filtered");
}
return {
retrievalResults: adjusted,
audit: {
candidate_evidence_total: candidateEvidenceTotal,
admissible_evidence_count: admissibleEvidenceCount,
rejected_evidence_count: rejectedEvidenceCount,
rejected_item_count: rejectedItemCount,
reject_breakdown: rejectBreakdown,
category_breakdown: categoryBreakdown,
reason_codes: uniqueStrings(reasonCodes)
}
};
}
function evaluateGroundedAnswerEligibility(input) {
const temporalPassed = input.temporal.temporal_guard_outcome === "passed";
const polarityPassed = !input.polarity.applied || input.polarity.outcome === "passed" || input.polarity.outcome === "not_applicable";
const admissibleEvidenceCount = input.evidence.admissible_evidence_count;
const criticalContradiction = Boolean(input.polarity.critical_contradiction);
const eligible = temporalPassed && polarityPassed && admissibleEvidenceCount > 0 && !criticalContradiction;
const reasonCodes = [];
if (!temporalPassed) {
reasonCodes.push(`temporal_guard_${input.temporal.temporal_guard_outcome}`);
}
if (!polarityPassed) {
reasonCodes.push(`polarity_guard_${input.polarity.outcome}`);
}
if (admissibleEvidenceCount <= 0) {
reasonCodes.push("admissible_evidence_count_zero");
}
if (criticalContradiction) {
reasonCodes.push("critical_domain_or_account_contradiction");
}
return {
eligible,
temporal_passed: temporalPassed,
polarity_passed: polarityPassed,
admissible_evidence_count: admissibleEvidenceCount,
critical_contradiction: criticalContradiction,
outcome: eligible ? "grounded_allowed" : "limited_or_insufficient_evidence",
reason_codes: uniqueStrings(reasonCodes)
};
}
function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
if (eligibility.eligible) {
return groundingCheck;
}
const status = eligibility.admissible_evidence_count <= 0 || !eligibility.temporal_passed ? "no_grounded_answer" : "partial";
const reasonMap = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
...eligibility.reason_codes.map((code) => reasonMap[code] ?? code)
];
return {
...groundingCheck,
status,
reasons: uniqueStrings(reasons)
};
}
+106 -6
View File
@@ -48,6 +48,7 @@ const investigationState_1 = __importStar(require("./investigationState"));
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
const questionTypeResolver_1 = __importStar(require("./questionTypeResolver"));
const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"));
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
function retrievalSummaryForRoute(route) {
if (route === "store_canonical")
return "Canonical accounting data path selected.";
@@ -897,6 +898,43 @@ function extractFollowupAccountAnchorsLoose(text) {
}
return Array.from(new Set(anchors));
}
function accountPrefixToken(value) {
const token = String(value ?? "").trim();
const match = token.match(/^(\d{2})/);
return match ? match[1] : null;
}
function hasCrossScopeConflictWithState(userMessage, state) {
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
const statePeriod = compactWhitespace(state.focus.period ?? "");
if (explicitPeriod && statePeriod && explicitPeriod !== statePeriod) {
return true;
}
const inferredDomain = inferP0DomainFromMessage(userMessage);
const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
if (inferredDomain && stateDomain && inferredDomain !== stateDomain) {
return true;
}
const explicitAccounts = extractAccountTokens(userMessage);
const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
const knownAccounts = Array.isArray(state.focus.primary_accounts) ? state.focus.primary_accounts : [];
if (fallbackAccounts.length > 0 && knownAccounts.length > 0) {
const knownPrefixes = new Set(knownAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
const newPrefixes = new Set(fallbackAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
if (newPrefixes.size > 0 && knownPrefixes.size > 0) {
let intersects = false;
for (const prefix of newPrefixes) {
if (knownPrefixes.has(prefix)) {
intersects = true;
break;
}
}
if (!intersects) {
return true;
}
}
}
return false;
}
function inferP0DomainFromMessage(text) {
const lower = String(text ?? "").toLowerCase();
const accountTokens = extractAccountTokens(lower);
@@ -1009,9 +1047,11 @@ function buildFollowupStateBinding(input) {
Boolean(problemState) &&
((problemState?.active_problem_units.length ?? 0) > 0 || (problemState?.focus_problem_types.length ?? 0) > 0);
const strongNewAnchorDetected = hasStrongFollowupAnchors(userMessage, input.investigationState);
const scopeConflictDetected = hasCrossScopeConflictWithState(userMessage, input.investigationState);
const periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable;
const shouldBind = !smallTalkSignal &&
!strongNewAnchorDetected &&
!scopeConflictDetected &&
(followupMarker || referentialPointer || periodRefinementFollowup || (!strongSignal && shortPrompt));
if (!shouldBind) {
return {
@@ -1116,7 +1156,10 @@ function buildFollowupStateBinding(input) {
problem_continuity_available: problemContinuityAvailable,
problem_continuity_applied: problemContinuityApplied,
problem_continuity_skipped_reason: problemContinuityApplied ? null : problemContinuitySkippedReason,
strong_new_anchor_detected: strongNewAnchorDetected
strong_new_anchor_detected: strongNewAnchorDetected,
scope_isolation_applied: true,
scope_carryover_allowed: !scopeConflictDetected,
scope_reset_reason: scopeConflictDetected ? "cross_scope_conflict" : null
}
}
};
@@ -1185,11 +1228,30 @@ class AssistantService {
useMock: Boolean(payload.useMock)
};
const normalized = await this.normalizerService.normalize(normalizePayload);
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
inferredDomainByMessage === "vat_document_register_book" ||
inferredDomainByMessage === "month_close_costs_20_44"
? inferredDomainByMessage
: null;
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
userMessage,
normalized: normalized.normalized,
companyAnchors
});
const domainPolarityGuardInitial = (0, assistantRuntimeGuards_1.resolveDomainPolarityGuard)({
userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
const executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
let executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
const retrievalCalls = [];
const retrievalResultsRaw = [];
const retrievalResults = [];
let retrievalResults = [];
for (const planItem of executionPlan) {
if (!planItem.should_execute) {
retrievalCalls.push({
@@ -1248,13 +1310,32 @@ class AssistantService {
retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
}
}
const polarityGuardResult = (0, assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults)({
retrievalResults,
guard: domainPolarityGuardInitial
});
retrievalResults = polarityGuardResult.retrievalResults;
const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({
retrievalResults,
temporal: temporalGuard,
focusDomainHint: focusDomainForGuards,
polarity: polarityGuardResult.audit.polarity,
companyAnchors,
userMessage
});
retrievalResults = evidenceGateResult.retrievalResults;
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
temporal: temporalGuard,
polarity: polarityGuardResult.audit,
evidence: evidenceGateResult.audit
});
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
const focusDomainHint = followupBinding.usage?.applied
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
: null;
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
@@ -1298,7 +1379,8 @@ class AssistantService {
requirements: coverageEvaluation.requirements,
coverageReport: coverageEvaluation.coverage,
retrievalResults,
replyType: composition.reply_type
replyType: composition.reply_type,
followupApplied: Boolean(followupBinding.usage?.applied)
})
: null;
if (config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) {
@@ -1326,6 +1408,15 @@ class AssistantService {
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
question_type_class: questionTypeClass,
company_anchors: companyAnchors,
raw_time_anchor: temporalGuard.raw_time_anchor,
resolved_time_anchor: temporalGuard.resolved_time_anchor,
temporal_resolution_source: temporalGuard.temporal_resolution_source,
temporal_guard_applied: temporalGuard.temporal_guard_applied,
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
domain_polarity_guard: polarityGuardResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
problem_units_used_count: composition.problem_units_used_count ?? 0,
@@ -1391,6 +1482,15 @@ class AssistantService {
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
question_type_class: questionTypeClass,
company_anchors: companyAnchors,
raw_time_anchor: temporalGuard.raw_time_anchor,
resolved_time_anchor: temporalGuard.resolved_time_anchor,
temporal_resolution_source: temporalGuard.temporal_resolution_source,
temporal_guard_applied: temporalGuard.temporal_guard_applied,
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
domain_polarity_guard: polarityGuardResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
problem_units_used_count: composition.problem_units_used_count ?? 0,
+95 -8
View File
@@ -23,6 +23,63 @@ function detectPeriod(text) {
return yearly[1];
return null;
}
function detectExplicitDomainHint(text) {
const messageCorpus = String(text ?? "").toLowerCase();
const accounts = detectAccounts(text);
const hasSettlementSignal = accounts.some((item) => isSettlementAccount(item)) ||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(messageCorpus);
if (hasSettlementSignal) {
return "settlements_60_62";
}
const hasVatSignal = accounts.some((item) => isVatAccount(item)) ||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
if (hasVatSignal) {
return "vat_document_register_book";
}
const hasCloseSignal = accounts.some((item) => isCloseCostsAccount(item)) ||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
if (hasCloseSignal) {
return "month_close_costs_20_44";
}
const hasFixedAssetSignal = accounts.some((item) => isFixedAssetAccount(item)) ||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(messageCorpus);
if (hasFixedAssetSignal) {
return "fixed_asset_amortization";
}
return null;
}
function buildQuestionScopeId(input) {
const domainPart = String(input.domain ?? "").trim();
const periodPart = String(input.period ?? "").trim();
const accountPart = capStrings(input.accounts.map((item) => String(item ?? "").trim()).filter(Boolean), 4).join(",");
const subjectPart = String(input.subject ?? "").trim().slice(0, 96).toLowerCase();
const parts = [
domainPart ? `d:${domainPart}` : "",
periodPart ? `p:${periodPart}` : "",
accountPart ? `a:${accountPart}` : "",
subjectPart ? `s:${subjectPart}` : ""
].filter(Boolean);
if (parts.length === 0) {
return null;
}
return parts.join("|");
}
function deriveScopeOrigin(input) {
if (input.followupApplied) {
return "followup_state_carryover";
}
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
const explicitDomain = detectExplicitDomainHint(input.userMessage);
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
return "explicit_from_message";
}
const routeDomain = deriveDomain(input.routeSummary);
if (routeDomain && routeDomain !== "no_route") {
return "route_derived";
}
return "underspecified";
}
function deriveDomain(routeSummary) {
if (!routeSummary)
return null;
@@ -105,7 +162,9 @@ function isCloseCostsAccount(value) {
}
function inferFollowupActiveDomain(input) {
const messageCorpus = String(input.userMessage ?? "").toLowerCase();
const contextualCorpus = `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
const contextualCorpus = input.allowStateCarryover
? `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase()
: messageCorpus;
const hasFixedAssetLexicalSignal = /(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(messageCorpus);
const hasFixedAssetAccountSignal = input.focusAccounts.some((item) => isFixedAssetAccount(item)) &&
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(messageCorpus);
@@ -127,7 +186,8 @@ function inferFollowupActiveDomain(input) {
if (hasCloseSignal) {
return "month_close_costs_20_44";
}
if (/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
if (input.allowStateCarryover &&
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
(input.previous.followup_context?.active_domain === "settlements_60_62" ||
input.previous.focus.domain === "settlements_60_62")) {
return "settlements_60_62";
@@ -136,7 +196,10 @@ function inferFollowupActiveDomain(input) {
if (routeDomain && routeDomain !== "no_route") {
return routeDomain;
}
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
if (input.allowStateCarryover) {
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
}
return null;
}
function collectUncoveredRequirementIds(coverageReport) {
return capStrings([
@@ -313,6 +376,8 @@ function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOSt
turn_index: 0,
updated_at: timestamp,
question_id: null,
question_scope_id: null,
scope_origin: null,
focus: {
domain: null,
period: null,
@@ -329,22 +394,40 @@ function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOSt
}
function updateInvestigationState(input) {
const previous = input.previous;
const followupApplied = input.followupApplied === true;
const focusFromMessage = capStrings(detectAccounts(input.userMessage), stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
const mergedFocusAccounts = capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
const mergedFocusAccounts = followupApplied
? capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS)
: capStrings(focusFromMessage, stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
const requirementIds = capStrings(input.requirements.map((item) => item.requirement_id), stage1Contracts_1.INVESTIGATION_MAX_REQUIREMENT_LINKS);
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
const problemUnitState = updateProblemUnitState(previous, input.retrievalResults);
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
const routeDomain = deriveDomain(input.routeSummary);
const activeDomain = inferFollowupActiveDomain({
userMessage: input.userMessage,
focusAccounts: focusFromMessage,
routeSummary: input.routeSummary,
previous
previous,
allowStateCarryover: followupApplied
});
const focusDomain = activeDomain ?? deriveDomain(input.routeSummary) ?? previous.focus.domain;
const focusDomain = activeDomain ?? routeDomain ?? (followupApplied ? previous.focus.domain : null);
const detectedPeriod = detectPeriod(input.userMessage);
const focusPeriod = detectedPeriod ?? (followupApplied ? previous.focus.period : null);
const settlementNextActions = settlementFocusActions(activeDomain);
const lastProblemUnitId = problemUnitState?.active_problem_units[0] ?? null;
const evidenceSummary = collectEvidenceSummary(input.retrievalResults);
const scopeOrigin = deriveScopeOrigin({
followupApplied,
userMessage: input.userMessage,
routeSummary: input.routeSummary
});
const questionScopeId = buildQuestionScopeId({
domain: focusDomain,
period: focusPeriod,
accounts: mergedFocusAccounts,
subject: mainRequirement
});
return {
schema_version: stage1Contracts_1.INVESTIGATION_STATE_SCHEMA_VERSION,
session_id: previous.session_id,
@@ -352,9 +435,11 @@ function updateInvestigationState(input) {
turn_index: previous.turn_index + 1,
updated_at: input.timestamp,
question_id: input.questionId,
question_scope_id: questionScopeId,
scope_origin: scopeOrigin,
focus: {
domain: focusDomain,
period: detectPeriod(input.userMessage) ?? previous.focus.period,
period: focusPeriod,
primary_accounts: mergedFocusAccounts,
active_query_subject: mainRequirement.slice(0, 180)
},
@@ -371,7 +456,9 @@ function updateInvestigationState(input) {
uncovered_requirement_ids: uncoveredRequirementIds,
last_problem_unit_id: lastProblemUnitId,
settlement_next_actions: settlementNextActions,
evidence_summary: evidenceSummary
evidence_summary: evidenceSummary,
question_scope_id: questionScopeId,
scope_origin: scopeOrigin
},
query_mode_hint: deriveQueryModeHint(input.routeSummary),
...(problemUnitState
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,7 @@ import * as investigationState_1 from "./investigationState";
import * as retrievalResultNormalizer_1 from "./retrievalResultNormalizer";
import * as questionTypeResolver_1 from "./questionTypeResolver";
import * as companyAnchorResolver_1 from "./companyAnchorResolver";
import * as assistantRuntimeGuards_1 from "./assistantRuntimeGuards";
function retrievalSummaryForRoute(route) {
if (route === "store_canonical")
return "Canonical accounting data path selected.";
@@ -859,6 +860,43 @@ function extractFollowupAccountAnchorsLoose(text) {
}
return Array.from(new Set(anchors));
}
function accountPrefixToken(value) {
const token = String(value ?? "").trim();
const match = token.match(/^(\d{2})/);
return match ? match[1] : null;
}
function hasCrossScopeConflictWithState(userMessage, state) {
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
const statePeriod = compactWhitespace(state.focus.period ?? "");
if (explicitPeriod && statePeriod && explicitPeriod !== statePeriod) {
return true;
}
const inferredDomain = inferP0DomainFromMessage(userMessage);
const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
if (inferredDomain && stateDomain && inferredDomain !== stateDomain) {
return true;
}
const explicitAccounts = extractAccountTokens(userMessage);
const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
const knownAccounts = Array.isArray(state.focus.primary_accounts) ? state.focus.primary_accounts : [];
if (fallbackAccounts.length > 0 && knownAccounts.length > 0) {
const knownPrefixes = new Set(knownAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
const newPrefixes = new Set(fallbackAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
if (newPrefixes.size > 0 && knownPrefixes.size > 0) {
let intersects = false;
for (const prefix of newPrefixes) {
if (knownPrefixes.has(prefix)) {
intersects = true;
break;
}
}
if (!intersects) {
return true;
}
}
}
return false;
}
function inferP0DomainFromMessage(text) {
const lower = String(text ?? "").toLowerCase();
const accountTokens = extractAccountTokens(lower);
@@ -971,9 +1009,11 @@ function buildFollowupStateBinding(input) {
Boolean(problemState) &&
((problemState?.active_problem_units.length ?? 0) > 0 || (problemState?.focus_problem_types.length ?? 0) > 0);
const strongNewAnchorDetected = hasStrongFollowupAnchors(userMessage, input.investigationState);
const scopeConflictDetected = hasCrossScopeConflictWithState(userMessage, input.investigationState);
const periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable;
const shouldBind = !smallTalkSignal &&
!strongNewAnchorDetected &&
!scopeConflictDetected &&
(followupMarker || referentialPointer || periodRefinementFollowup || (!strongSignal && shortPrompt));
if (!shouldBind) {
return {
@@ -1078,7 +1118,10 @@ function buildFollowupStateBinding(input) {
problem_continuity_available: problemContinuityAvailable,
problem_continuity_applied: problemContinuityApplied,
problem_continuity_skipped_reason: problemContinuityApplied ? null : problemContinuitySkippedReason,
strong_new_anchor_detected: strongNewAnchorDetected
strong_new_anchor_detected: strongNewAnchorDetected,
scope_isolation_applied: true,
scope_carryover_allowed: !scopeConflictDetected,
scope_reset_reason: scopeConflictDetected ? "cross_scope_conflict" : null
}
}
};
@@ -1147,11 +1190,30 @@ export class AssistantService {
useMock: Boolean(payload.useMock)
};
const normalized = await this.normalizerService.normalize(normalizePayload);
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
inferredDomainByMessage === "vat_document_register_book" ||
inferredDomainByMessage === "month_close_costs_20_44"
? inferredDomainByMessage
: null;
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
userMessage,
normalized: normalized.normalized,
companyAnchors
});
const domainPolarityGuardInitial = (0, assistantRuntimeGuards_1.resolveDomainPolarityGuard)({
userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
const executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
let executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
const retrievalCalls = [];
const retrievalResultsRaw = [];
const retrievalResults = [];
let retrievalResults = [];
for (const planItem of executionPlan) {
if (!planItem.should_execute) {
retrievalCalls.push({
@@ -1210,13 +1272,32 @@ export class AssistantService {
retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
}
}
const polarityGuardResult = (0, assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults)({
retrievalResults,
guard: domainPolarityGuardInitial
});
retrievalResults = polarityGuardResult.retrievalResults;
const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({
retrievalResults,
temporal: temporalGuard,
focusDomainHint: focusDomainForGuards,
polarity: polarityGuardResult.audit.polarity,
companyAnchors,
userMessage
});
retrievalResults = evidenceGateResult.retrievalResults;
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
temporal: temporalGuard,
polarity: polarityGuardResult.audit,
evidence: evidenceGateResult.audit
});
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
const focusDomainHint = followupBinding.usage?.applied
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
: null;
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
@@ -1260,7 +1341,8 @@ export class AssistantService {
requirements: coverageEvaluation.requirements,
coverageReport: coverageEvaluation.coverage,
retrievalResults,
replyType: composition.reply_type
replyType: composition.reply_type,
followupApplied: Boolean(followupBinding.usage?.applied)
})
: null;
if (config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) {
@@ -1288,6 +1370,15 @@ export class AssistantService {
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
question_type_class: questionTypeClass,
company_anchors: companyAnchors,
raw_time_anchor: temporalGuard.raw_time_anchor,
resolved_time_anchor: temporalGuard.resolved_time_anchor,
temporal_resolution_source: temporalGuard.temporal_resolution_source,
temporal_guard_applied: temporalGuard.temporal_guard_applied,
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
domain_polarity_guard: polarityGuardResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
problem_units_used_count: composition.problem_units_used_count ?? 0,
@@ -1353,6 +1444,15 @@ export class AssistantService {
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
question_type_class: questionTypeClass,
company_anchors: companyAnchors,
raw_time_anchor: temporalGuard.raw_time_anchor,
resolved_time_anchor: temporalGuard.resolved_time_anchor,
temporal_resolution_source: temporalGuard.temporal_resolution_source,
temporal_guard_applied: temporalGuard.temporal_guard_applied,
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
temporal_guard: temporalGuard,
domain_polarity_guard: polarityGuardResult.audit,
evidence_admissibility_gate: evidenceGateResult.audit,
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
problem_units_used_count: composition.problem_units_used_count ?? 0,
@@ -7,6 +7,7 @@ import type { RouteHintSummary } from "../types/normalizer";
import type {
InvestigationLastAnswerMode,
InvestigationNarrowingStatus,
InvestigationScopeOrigin,
InvestigationState
} from "../types/stage1Contracts";
import {
@@ -39,6 +40,7 @@ interface UpdateInvestigationStateInput {
coverageReport: RequirementCoverageReport;
retrievalResults: UnifiedRetrievalResult[];
replyType: InvestigationLastAnswerMode;
followupApplied?: boolean;
}
function uniqueStrings(values: string[]): string[] {
@@ -61,6 +63,83 @@ function detectPeriod(text: string): string | null {
return null;
}
function detectExplicitDomainHint(text: string): string | null {
const messageCorpus = String(text ?? "").toLowerCase();
const accounts = detectAccounts(text);
const hasSettlementSignal =
accounts.some((item) => isSettlementAccount(item)) ||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
messageCorpus
);
if (hasSettlementSignal) {
return "settlements_60_62";
}
const hasVatSignal =
accounts.some((item) => isVatAccount(item)) ||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
if (hasVatSignal) {
return "vat_document_register_book";
}
const hasCloseSignal =
accounts.some((item) => isCloseCostsAccount(item)) ||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
if (hasCloseSignal) {
return "month_close_costs_20_44";
}
const hasFixedAssetSignal =
accounts.some((item) => isFixedAssetAccount(item)) ||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
messageCorpus
);
if (hasFixedAssetSignal) {
return "fixed_asset_amortization";
}
return null;
}
function buildQuestionScopeId(input: {
domain: string | null;
period: string | null;
accounts: string[];
subject: string;
}): string | null {
const domainPart = String(input.domain ?? "").trim();
const periodPart = String(input.period ?? "").trim();
const accountPart = capStrings(input.accounts.map((item) => String(item ?? "").trim()).filter(Boolean), 4).join(",");
const subjectPart = String(input.subject ?? "").trim().slice(0, 96).toLowerCase();
const parts = [
domainPart ? `d:${domainPart}` : "",
periodPart ? `p:${periodPart}` : "",
accountPart ? `a:${accountPart}` : "",
subjectPart ? `s:${subjectPart}` : ""
].filter(Boolean);
if (parts.length === 0) {
return null;
}
return parts.join("|");
}
function deriveScopeOrigin(input: {
followupApplied: boolean;
userMessage: string;
routeSummary: RouteHintSummary | null;
}): InvestigationScopeOrigin {
if (input.followupApplied) {
return "followup_state_carryover";
}
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
const explicitDomain = detectExplicitDomainHint(input.userMessage);
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
return "explicit_from_message";
}
const routeDomain = deriveDomain(input.routeSummary);
if (routeDomain && routeDomain !== "no_route") {
return "route_derived";
}
return "underspecified";
}
function deriveDomain(routeSummary: RouteHintSummary | null): string | null {
if (!routeSummary) return null;
if (routeSummary.mode === "legacy_v1") {
@@ -165,9 +244,12 @@ function inferFollowupActiveDomain(input: {
focusAccounts: string[];
routeSummary: RouteHintSummary | null;
previous: InvestigationStateWithProblemUnits;
allowStateCarryover: boolean;
}): string | null {
const messageCorpus = String(input.userMessage ?? "").toLowerCase();
const contextualCorpus = `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
const contextualCorpus = input.allowStateCarryover
? `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase()
: messageCorpus;
const hasFixedAssetLexicalSignal =
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
@@ -206,6 +288,7 @@ function inferFollowupActiveDomain(input: {
}
if (
input.allowStateCarryover &&
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
(input.previous.followup_context?.active_domain === "settlements_60_62" ||
input.previous.focus.domain === "settlements_60_62")
@@ -218,7 +301,11 @@ function inferFollowupActiveDomain(input: {
return routeDomain;
}
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
if (input.allowStateCarryover) {
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
}
return null;
}
function collectUncoveredRequirementIds(coverageReport: RequirementCoverageReport): string[] {
@@ -447,6 +534,8 @@ export function createEmptyInvestigationState(
turn_index: 0,
updated_at: timestamp,
question_id: null,
question_scope_id: null,
scope_origin: null,
focus: {
domain: null,
period: null,
@@ -464,11 +553,11 @@ export function createEmptyInvestigationState(
export function updateInvestigationState(input: UpdateInvestigationStateInput): InvestigationStateWithProblemUnits {
const previous = input.previous;
const followupApplied = input.followupApplied === true;
const focusFromMessage = capStrings(detectAccounts(input.userMessage), INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
const mergedFocusAccounts = capStrings(
[...focusFromMessage, ...previous.focus.primary_accounts],
INVESTIGATION_MAX_PRIMARY_ACCOUNTS
);
const mergedFocusAccounts = followupApplied
? capStrings([...focusFromMessage, ...previous.focus.primary_accounts], INVESTIGATION_MAX_PRIMARY_ACCOUNTS)
: capStrings(focusFromMessage, INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
const requirementIds = capStrings(
input.requirements.map((item) => item.requirement_id),
INVESTIGATION_MAX_REQUIREMENT_LINKS
@@ -476,16 +565,31 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
const problemUnitState = updateProblemUnitState(previous, input.retrievalResults);
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
const routeDomain = deriveDomain(input.routeSummary);
const activeDomain = inferFollowupActiveDomain({
userMessage: input.userMessage,
focusAccounts: focusFromMessage,
routeSummary: input.routeSummary,
previous
previous,
allowStateCarryover: followupApplied
});
const focusDomain = activeDomain ?? deriveDomain(input.routeSummary) ?? previous.focus.domain;
const focusDomain = activeDomain ?? routeDomain ?? (followupApplied ? previous.focus.domain : null);
const detectedPeriod = detectPeriod(input.userMessage);
const focusPeriod = detectedPeriod ?? (followupApplied ? previous.focus.period : null);
const settlementNextActions = settlementFocusActions(activeDomain);
const lastProblemUnitId = problemUnitState?.active_problem_units[0] ?? null;
const evidenceSummary = collectEvidenceSummary(input.retrievalResults);
const scopeOrigin = deriveScopeOrigin({
followupApplied,
userMessage: input.userMessage,
routeSummary: input.routeSummary
});
const questionScopeId = buildQuestionScopeId({
domain: focusDomain,
period: focusPeriod,
accounts: mergedFocusAccounts,
subject: mainRequirement
});
return {
schema_version: INVESTIGATION_STATE_SCHEMA_VERSION,
@@ -494,9 +598,11 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
turn_index: previous.turn_index + 1,
updated_at: input.timestamp,
question_id: input.questionId,
question_scope_id: questionScopeId,
scope_origin: scopeOrigin,
focus: {
domain: focusDomain,
period: detectPeriod(input.userMessage) ?? previous.focus.period,
period: focusPeriod,
primary_accounts: mergedFocusAccounts,
active_query_subject: mainRequirement.slice(0, 180)
},
@@ -516,7 +622,9 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
uncovered_requirement_ids: uncoveredRequirementIds,
last_problem_unit_id: lastProblemUnitId,
settlement_next_actions: settlementNextActions,
evidence_summary: evidenceSummary
evidence_summary: evidenceSummary,
question_scope_id: questionScopeId,
scope_origin: scopeOrigin
},
query_mode_hint: deriveQueryModeHint(input.routeSummary),
...(problemUnitState
@@ -67,9 +67,66 @@ export interface FollowupStateUsageDebug {
problem_continuity_applied?: boolean;
problem_continuity_skipped_reason?: string | null;
strong_new_anchor_detected?: boolean;
scope_isolation_applied?: boolean;
scope_carryover_allowed?: boolean;
scope_reset_reason?: string | null;
};
}
export interface TemporalGuardDebug {
raw_time_anchor: string | null;
resolved_time_anchor: string | null;
temporal_resolution_source: string;
temporal_guard_applied: boolean;
temporal_guard_outcome: "passed" | "failed_out_of_snapshot_window" | "ambiguous_limited";
primary_period_window: {
from: string;
to: string;
granularity: "day" | "month";
} | null;
reason_codes: string[];
}
export interface DomainPolarityGuardDebug {
applied: boolean;
polarity: "supplier_payable" | "customer_receivable" | "mixed_or_unresolved" | "not_applicable";
outcome: "passed" | "limited_unresolved_polarity" | "blocked_conflict" | "not_applicable";
supplier_score: number;
customer_score: number;
account_scope: string[];
rejected_problem_units: number;
rejected_evidence: number;
critical_contradiction: boolean;
reason_codes: string[];
}
export interface EvidenceAdmissibilityGateDebug {
candidate_evidence_total: number;
admissible_evidence_count: number;
rejected_evidence_count: number;
rejected_item_count: number;
reject_breakdown: Record<
"wrong_period" | "wrong_domain" | "wrong_account_scope" | "weak_source_mapping" | "zero_live_match" | "future_dated_or_out_of_window",
number
>;
category_breakdown: {
hard_evidence: number;
supporting_signal: number;
inadmissible_noise: number;
};
reason_codes: string[];
}
export interface GroundedAnswerEligibilityGuardDebug {
eligible: boolean;
temporal_passed: boolean;
polarity_passed: boolean;
admissible_evidence_count: number;
critical_contradiction: boolean;
outcome: "grounded_allowed" | "limited_or_insufficient_evidence";
reason_codes: string[];
}
export interface AssistantMessageRequestPayload {
session_id?: string;
user_message?: string;
@@ -132,6 +189,15 @@ export interface AssistantDebugPayload {
retrieval_results: UnifiedRetrievalResult[];
answer_grounding_check: AnswerGroundingCheck;
dropped_intent_segments: string[];
raw_time_anchor?: string | null;
resolved_time_anchor?: string | null;
temporal_resolution_source?: string;
temporal_guard_applied?: boolean;
temporal_guard_outcome?: TemporalGuardDebug["temporal_guard_outcome"];
temporal_guard?: TemporalGuardDebug;
domain_polarity_guard?: DomainPolarityGuardDebug;
evidence_admissibility_gate?: EvidenceAdmissibilityGateDebug;
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
followup_state_usage?: FollowupStateUsageDebug;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
@@ -10,6 +10,11 @@ export const INVESTIGATION_MAX_REQUIREMENT_LINKS = 8;
export type InvestigationNarrowingStatus = "unknown" | "not_needed" | "applied" | "needs_clarification" | "broad_guarded";
export type InvestigationQueryModeHint = "direct_answer" | "investigation_candidate";
export type InvestigationScopeOrigin =
| "explicit_from_message"
| "followup_state_carryover"
| "route_derived"
| "underspecified";
export type InvestigationLastAnswerMode =
| "factual"
| "factual_with_explanation"
@@ -39,6 +44,8 @@ export interface InvestigationFollowupContext {
last_problem_unit_id?: string | null;
settlement_next_actions?: string[];
evidence_summary?: string[];
question_scope_id?: string | null;
scope_origin?: InvestigationScopeOrigin | null;
}
export interface InvestigationState {
@@ -48,6 +55,8 @@ export interface InvestigationState {
turn_index: number;
updated_at: string;
question_id: string | null;
question_scope_id?: string | null;
scope_origin?: InvestigationScopeOrigin | null;
focus: InvestigationStateFocus;
narrowing_status: InvestigationNarrowingStatus;
evidence_refs: string[];
@@ -28,6 +28,11 @@ describe("assistant mode API", () => {
expect(typeof response.body.debug?.answer_grounding_check?.status).toBe("string");
expect(Array.isArray(response.body.debug?.retrieval_status)).toBe(true);
expect(Array.isArray(response.body.debug?.retrieval_results)).toBe(true);
expect(typeof response.body.debug?.temporal_guard_applied).toBe("boolean");
expect(typeof response.body.debug?.temporal_guard_outcome).toBe("string");
expect(response.body.debug?.domain_polarity_guard).toBeTruthy();
expect(response.body.debug?.evidence_admissibility_gate).toBeTruthy();
expect(response.body.debug?.grounded_answer_eligibility_guard).toBeTruthy();
expect(Array.isArray(response.body.conversation)).toBe(true);
expect(response.body.conversation.length).toBe(2);
});
@@ -133,7 +138,7 @@ describe("assistant mode API", () => {
expect(response.status).toBe(200);
expect(response.body.reply_type).not.toBe("route_mismatch_blocked");
expect(response.body.debug?.answer_grounding_check?.status).not.toBe("route_mismatch_blocked");
expect(["partial", "grounded"]).toContain(String(response.body.debug?.answer_grounding_check?.status));
expect(["partial", "grounded", "no_grounded_answer"]).toContain(String(response.body.debug?.answer_grounding_check?.status));
expect(response.body.reply_type).toBe("partial_coverage");
});
@@ -150,7 +155,7 @@ describe("assistant mode API", () => {
expect(["partial_coverage", "clarification_required", "route_mismatch_blocked", "factual_with_explanation"]).toContain(
String(response.body.reply_type)
);
expect(["partial", "grounded", "route_mismatch_blocked"]).toContain(
expect(["partial", "grounded", "route_mismatch_blocked", "no_grounded_answer"]).toContain(
String(response.body.debug?.answer_grounding_check?.status)
);
expect(typeof response.body.debug?.answer_grounding_check?.route_subject_match).toBe("boolean");
@@ -194,6 +194,46 @@ describe.sequential("assistant follow-up state binding", () => {
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
});
it("isolates scope for independent cross-domain turn and does not carry stale period/domain", async () => {
const app = await createAppWithFlags({
state: "1",
binding: "1",
problemUnits: "1",
continuity: "1",
answerPolicy: "1",
problemCentric: "1"
});
const sessionId = `asst-wave17-scope-isolation-${Date.now()}`;
const first = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь хвосты по счету 60.01 за 2020-06 и почему не закрылся долг."
});
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.question_scope_id).toContain("d:settlements_60_62");
expect(first.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
const second = await request(app).post("/api/assistant/message").send({
session_id: sessionId,
useMock: true,
promptVersion: "normalizer_v2_0_2",
user_message: "Проверь НДС по счету-фактуре: где разрыв в цепочке документ -> регистр -> книга?"
});
expect(second.status).toBe(200);
expect(second.body.debug?.followup_state_usage).toBeUndefined();
expect(second.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
expect(String(second.body.debug?.investigation_state_snapshot?.question_scope_id ?? "")).toContain(
"d:vat_document_register_book"
);
expect(second.body.debug?.investigation_state_snapshot?.focus?.period).not.toBe("2020-06");
expect(String(second.body.debug?.investigation_state_snapshot?.focus?.domain ?? "")).not.toContain("settlements_60_62");
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.question_scope_id).toBeTruthy();
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.scope_origin).toBe("explicit_from_message");
});
it("rebinds follow-up domain away from settlements on fixed-asset amortization query", async () => {
const app = await createAppWithFlags({
state: "1",
@@ -0,0 +1,359 @@
import { describe, expect, it } from "vitest";
import { resolveCompanyAnchors } from "../src/services/companyAnchorResolver";
import {
applyDomainPolarityGuardToRetrievalResults,
applyEligibilityToGroundingCheck,
applyEvidenceAdmissibilityGate,
applyPolarityHintToExecutionPlan,
applyTemporalHintToExecutionPlan,
evaluateGroundedAnswerEligibility,
resolveDomainPolarityGuard,
resolveTemporalGuard
} from "../src/services/assistantRuntimeGuards";
function buildProblemUnit(input: {
id: string;
lifecycleDomain: string;
defect?: string;
account?: string;
}): any {
return {
schema_version: "problem_unit_v0_1",
problem_unit_id: input.id,
problem_unit_type: "unresolved_settlement_cluster",
title: "test unit",
mechanism_summary: input.defect ?? "mechanism",
business_defect_class: input.defect ?? "mechanism",
severity: { score: 0.8, grade: "high" },
confidence: { score: 0.7, grade: "medium" },
lifecycle_domain: input.lifecycleDomain,
affected_entities: ["Document:DOC-1"],
affected_documents: ["Document:DOC-1"],
affected_postings: ["Posting:POST-1"],
affected_accounts: [input.account ?? "60"],
affected_counterparties: ["Counterparty:CP-1"],
affected_contracts: ["Contract:CTR-1"],
failed_expected_edge: "payment_to_settlement",
period_impact: { is_period_sensitive: true, impact_class: "close_risk" },
evidence_pack: ["cand-1"],
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
snapshot_limitations: []
};
}
function buildEvidence(input: {
id: string;
period: string;
accountDebit?: string;
accountCredit?: string;
limitationCode?: string | null;
sourceLayer?: string | null;
}): any {
return {
evidence_id: input.id,
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "unknown",
entity: "MCPLiveMovement",
id: input.id,
period: input.period,
canonical_ref: `evidence_source_ref_v1|unknown|mcplivemovement|${String(input.id).toLowerCase()}|${input.period}`
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "unknown",
entity: "MCPLiveMovement",
id: input.id,
period: input.period
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "payment_to_settlement",
confidence: "medium",
limitation: input.limitationCode
? {
reason_code: input.limitationCode,
note: null
}
: null,
payload: {
period: input.period,
account_debit: input.accountDebit ?? null,
account_credit: input.accountCredit ?? null,
source_layer: input.sourceLayer ?? null
}
};
}
function buildRetrieval(input?: Partial<any>): any {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "chain",
items: [],
summary: {
semantic_profile: {
account_scope: ["60"],
domain_scope: ["settlements", "customers"],
relation_patterns: ["payment_to_settlement"]
},
live_mcp: {
matched_rows: 0,
account_scope: ["60"]
}
},
evidence: [],
candidate_evidence: [],
problem_units: [],
problem_unit_summary: null,
why_included: ["test"],
selection_reason: ["test"],
risk_factors: ["test"],
business_interpretation: ["test"],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("stage4 blocker-pack runtime guards", () => {
it("flags temporal anchor drift outside July 2020 snapshot", () => {
const userMessage = "Почему по оплате от 6 июля 2020 долг по поставщику остался?";
const temporal = resolveTemporalGuard({
userMessage,
companyAnchors: resolveCompanyAnchors(userMessage),
normalized: {
schema_version: "normalized_query_v2_0_2",
fragments: [
{
fragment_id: "F1",
time_scope: {
type: "explicit",
value: "2023-07-06",
confidence: "high"
}
}
]
} as any
});
expect(temporal.temporal_guard_applied).toBe(true);
expect(temporal.temporal_guard_outcome).toBe("failed_out_of_snapshot_window");
expect(temporal.resolved_time_anchor).toBe("2020-07-06");
expect(temporal.reason_codes).toContain("normalized_anchor_out_of_snapshot_window");
});
it("locks July month window when question has month-only anchor", () => {
const userMessage = "В июльском срезе почему по счету 60 остался хвост?";
const temporal = resolveTemporalGuard({
userMessage,
companyAnchors: resolveCompanyAnchors(userMessage),
normalized: {
schema_version: "normalized_query_v2_0_2",
fragments: []
} as any
});
const hintedPlan = applyTemporalHintToExecutionPlan(
[
{
should_execute: true,
fragment_text: "проверить зависший долг по поставщику"
}
],
temporal
);
expect(temporal.temporal_guard_applied).toBe(true);
expect(temporal.temporal_guard_outcome).toBe("passed");
expect(temporal.resolved_time_anchor).toBe("2020-07");
expect(hintedPlan[0].fragment_text).toMatch(/июля 2020|2020-07-01/);
});
it("filters customer settlement semantics from supplier/payable case", () => {
const guard = resolveDomainPolarityGuard({
userMessage: "По поставщику и счету 60 долг не закрылся после оплаты.",
focusDomainHint: "settlements_60_62"
});
const withHint = applyPolarityHintToExecutionPlan(
[
{
should_execute: true,
fragment_text: "проверить цепочку закрытия долга"
}
],
guard
);
const result = applyDomainPolarityGuardToRetrievalResults({
guard,
retrievalResults: [
buildRetrieval({
problem_units: [
buildProblemUnit({
id: "pu-supplier",
lifecycleDomain: "bank_settlement",
defect: "payment_to_settlement",
account: "60"
}),
buildProblemUnit({
id: "pu-customer",
lifecycleDomain: "customer_settlement",
defect: "stale_receivable",
account: "62"
})
],
evidence: [
buildEvidence({
id: "ev-supplier",
period: "2020-07",
accountDebit: "60.01"
}),
buildEvidence({
id: "ev-customer",
period: "2020-07",
accountDebit: "62.01"
})
]
})
]
});
expect(guard.polarity).toBe("supplier_payable");
expect(withHint[0].fragment_text).toMatch(/счет 60|поставщиком/i);
expect(result.audit.outcome).toBe("passed");
expect(result.audit.rejected_problem_units).toBeGreaterThan(0);
expect(result.audit.rejected_evidence).toBeGreaterThan(0);
const units = result.retrievalResults[0].problem_units ?? [];
expect(units.some((item: any) => item.lifecycle_domain === "customer_settlement")).toBe(false);
});
it("rejects inadmissible live evidence on zero matched_rows and wrong account/date", () => {
const userMessage = "Почему по поставщику по счету 60 в июле 2020 хвост не закрыт?";
const temporal = resolveTemporalGuard({
userMessage,
companyAnchors: resolveCompanyAnchors(userMessage),
normalized: {
schema_version: "normalized_query_v2_0_2",
fragments: [
{
fragment_id: "F1",
time_scope: {
type: "explicit",
value: "2020-07",
confidence: "high"
}
}
]
} as any
});
const polarity = resolveDomainPolarityGuard({
userMessage,
focusDomainHint: "settlements_60_62"
});
const gated = applyEvidenceAdmissibilityGate({
retrievalResults: [
buildRetrieval({
items: [
{
source_entity: "MCPLiveMovement",
source_layer: "mcp_live_probe",
account_debit: "68.02",
account_credit: "19.04",
period: "2026-02-01"
}
],
evidence: [
buildEvidence({
id: "ev-live-1",
period: "2026-02-01",
accountDebit: "68.02",
accountCredit: "19.04",
limitationCode: "weak_source_mapping",
sourceLayer: "mcp_live_probe"
})
]
})
],
temporal,
polarity: polarity.polarity,
focusDomainHint: "settlements_60_62",
userMessage,
companyAnchors: resolveCompanyAnchors(userMessage)
});
expect(gated.audit.admissible_evidence_count).toBe(0);
expect(gated.audit.rejected_evidence_count).toBe(1);
expect(gated.audit.reject_breakdown.zero_live_match).toBeGreaterThan(0);
expect(gated.audit.reject_breakdown.wrong_account_scope).toBeGreaterThan(0);
expect(gated.audit.reject_breakdown.future_dated_or_out_of_window).toBeGreaterThan(0);
expect(gated.retrievalResults[0].evidence).toHaveLength(0);
expect(gated.retrievalResults[0].items).toHaveLength(0);
});
it("degrades grounded status when eligibility guard fails", () => {
const eligibility = evaluateGroundedAnswerEligibility({
temporal: {
raw_time_anchor: "6 июля 2020",
resolved_time_anchor: "2020-07-06",
temporal_resolution_source: "company_snapshot_july_day_lock",
temporal_guard_applied: true,
temporal_guard_outcome: "failed_out_of_snapshot_window",
primary_period_window: {
from: "2020-07-06",
to: "2020-07-06",
granularity: "day"
},
reason_codes: ["normalized_anchor_out_of_snapshot_window"]
},
polarity: {
applied: true,
polarity: "supplier_payable",
outcome: "passed",
supplier_score: 3,
customer_score: 0,
account_scope: ["60"],
rejected_problem_units: 0,
rejected_evidence: 0,
critical_contradiction: false,
reason_codes: []
},
evidence: {
candidate_evidence_total: 2,
admissible_evidence_count: 0,
rejected_evidence_count: 2,
rejected_item_count: 1,
reject_breakdown: {
wrong_period: 1,
wrong_domain: 0,
wrong_account_scope: 1,
weak_source_mapping: 0,
zero_live_match: 0,
future_dated_or_out_of_window: 1
},
category_breakdown: {
hard_evidence: 0,
supporting_signal: 0,
inadmissible_noise: 2
},
reason_codes: ["no_admissible_evidence_for_grounded_answer"]
}
});
const grounded = applyEligibilityToGroundingCheck(
{
status: "grounded",
reasons: []
},
eligibility
);
expect(eligibility.eligible).toBe(false);
expect(eligibility.reason_codes).toContain("admissible_evidence_count_zero");
expect(grounded.status).toBe("no_grounded_answer");
expect(grounded.reasons.join(" ")).toMatch(/Недостаточно допустимого evidence|Temporal anchor/i);
});
});
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import {
buildConversationExportForCopy,
sanitizeConversationExportText,
type ConversationExportItem
} from "../../frontend/src/utils/conversationExport";
function buildConversation(): ConversationExportItem[] {
return [
{
message_id: "m-user-1",
role: "user",
text: "Почему не закрывается РБП за июль?",
reply_type: null,
created_at: "2026-03-28T18:00:00.000Z",
trace_id: null,
debug: null
},
{
message_id: "m-asst-1",
role: "assistant",
text: [
"Коротко: есть признаки неполного списания РБП.",
"",
"### debug_payload_json",
"```json",
"{\"route_summary\":\"hybrid_store_plus_live\",\"domain_scope\":[\"deferred_expense\"]}",
"```"
].join("\n"),
reply_type: "partial_coverage",
created_at: "2026-03-28T18:00:05.000Z",
trace_id: "tr-1",
debug: {
route_summary: "hybrid_store_plus_live",
coverage_report: { requirements_covered: 0 }
}
}
];
}
describe("conversation export regression", () => {
it("default export excludes debug payload sections and keeps user-facing text", () => {
const exportText = buildConversationExportForCopy("sess-1", buildConversation(), "default");
expect(exportText).toContain("export_mode: default");
expect(exportText).toContain("Коротко: есть признаки неполного списания РБП.");
expect(exportText).not.toContain("### debug_payload_json");
expect(exportText).not.toContain("### technical_debug_payload_json");
expect(exportText).not.toContain("coverage_report");
expect(exportText).not.toContain("domain_scope");
expect(exportText).not.toContain("route_summary");
});
it("technical export includes debug payload in dedicated section only", () => {
const exportText = buildConversationExportForCopy("sess-1", buildConversation(), "technical");
expect(exportText).toContain("export_mode: technical");
expect(exportText).toContain("### technical_debug_payload_json");
expect(exportText).toContain("\"route_summary\": \"hybrid_store_plus_live\"");
expect(exportText).not.toContain("### debug_payload_json");
});
it("sanitizeConversationExportText removes technical tails", () => {
const source = [
"Ответ для пользователя.",
"domain_scope: [vat_flow]",
"coverage_report: {...}",
"### technical_breakdown_json",
"```json",
"{\"internal\":\"yes\"}",
"```"
].join("\n");
const sanitized = sanitizeConversationExportText(source);
expect(sanitized).toContain("Ответ для пользователя.");
expect(sanitized).not.toContain("domain_scope");
expect(sanitized).not.toContain("coverage_report");
expect(sanitized).not.toContain("technical_breakdown_json");
expect(sanitized).not.toContain("internal");
});
});
@@ -17,6 +17,8 @@ describe("investigation_state flow scaffolding", () => {
expect(first.status).toBe(200);
expect(first.body.debug?.investigation_state_snapshot?.schema_version).toBe("investigation_state_v1");
expect(first.body.debug?.investigation_state_snapshot?.question_scope_id).toBeTruthy();
expect(first.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
expect(first.body.debug?.answer_structure_v11?.schema_version).toBe("answer_structure_v1_1");
expect(first.body.debug?.followup_state_usage).toBeUndefined();
@@ -38,6 +40,8 @@ describe("investigation_state flow scaffolding", () => {
expect(second.status).toBe(200);
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
expect(second.body.debug?.investigation_state_snapshot?.question_scope_id).toBeTruthy();
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.question_scope_id).toBeTruthy();
const sessionResponse = await request(app).get(`/api/assistant/session/${sessionId}`);
expect(sessionResponse.status).toBe(200);