ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.2 - Усилена оркестрацию в deep/address гейте и follow-up binding в assistantService.ts. Починен кейс UTF-8 follow-up refinement (теперь followup_state_usage.applied=true в нужном сценарии). Убраны регрессии по assistantLivingRouter и stage3 lifecycle probe. Корректное поведение для llm canonical candidate (чтобы не уезжало в clarification_required там, где должен быть address factual).

This commit is contained in:
2026-04-11 14:56:14 +03:00
parent 19f5f19d8e
commit b5bd4fd737
46 changed files with 2471 additions and 370 deletions
@@ -4,6 +4,7 @@ import type { CompanyAnchorSet } from "./companyAnchorResolver";
import type { EvidenceItem } from "../types/stage1Contracts";
import type { ProblemUnit } from "../types/stage2ProblemUnits";
import type { ClaimBoundAnchorAudit } from "./assistantClaimBoundEvidence";
import iconv from "iconv-lite";
type P0DomainHint =
| "settlements_60_62"
@@ -895,12 +896,70 @@ export interface DomainPolarityGuardAudit {
reason_codes: string[];
}
function mojibakeScoreForRuntimeGuards(value: string): number {
const source = String(value ?? "");
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
const hardMarkers = (source.match(/[ѓ“‚„…†‡€‰‹‰ЉЊ‹Џ‘’“”•–—™љ›њћџ]/g) ?? []).length;
const pairMarkers = (source.match(/(?:Р.|С.|Гђ.|Г‘.)/g) ?? []).length;
const doubleEncodedMarkers = (source.match(/(?:Р“.|Р’.|Гѓ.|Г‚.)/gu) ?? []).length;
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
}
function looksLikeMojibakeForRuntimeGuards(value: string): boolean {
const source = String(value ?? "");
if (!source.trim()) {
return false;
}
if (/[ѓ“‚„…†‡€‰‹‰ЉЊ‹Џ‘’“”•–—™љ›њћџ]/.test(source)) {
return true;
}
if ((source.match(/(?:Р.|С.|Гђ.|Г‘.)/g) ?? []).length >= 2) {
return true;
}
return (source.match(/(?:Р“.|Р’.|Гѓ.|Г‚.)/gu) ?? []).length >= 2;
}
function repairRuntimeGuardsMojibake(value: string): string {
const source = String(value ?? "");
if (!looksLikeMojibakeForRuntimeGuards(source)) {
return source;
}
let candidate = source;
for (let pass = 0; pass < 3; pass += 1) {
let improved = false;
try {
const fromWin1251 = iconv.encode(candidate, "win1251").toString("utf8");
if (mojibakeScoreForRuntimeGuards(fromWin1251) > mojibakeScoreForRuntimeGuards(candidate)) {
candidate = fromWin1251;
improved = true;
}
} catch (_error) {
// noop
}
try {
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
if (mojibakeScoreForRuntimeGuards(fromLatin1) > mojibakeScoreForRuntimeGuards(candidate)) {
candidate = fromLatin1;
improved = true;
}
} catch (_error) {
// noop
}
if (!improved) {
break;
}
}
return candidate;
}
export function resolveDomainPolarityGuard(input: {
userMessage: string;
companyAnchors?: CompanyAnchorSet | null;
focusDomainHint?: string | null;
}): DomainPolarityGuardAudit {
const lower = String(input.userMessage ?? "").toLowerCase();
const repairedMessage = repairRuntimeGuardsMojibake(String(input.userMessage ?? ""));
const lower = repairedMessage.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): item is string => Boolean(item)));
@@ -1683,7 +1742,8 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
const reasonMap: Record<string, string> = {
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
temporal_guard_failed_out_of_snapshot_window:
"Запрошенный период выходит за доступный срез данных. Temporal anchor outside snapshot window.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",