Этап 4 / Волна 10: корректировка settlement-кейса — доменная фиксация синтеза, честное покрытие, удержание фокуса / Этап 4 / Волна 11: бизнес-якоря, доменное заземление и устранение утечки дебага

This commit is contained in:
2026-03-28 02:17:19 +03:00
parent 914843a8ba
commit a06e575be4
367 changed files with 432257 additions and 3627 deletions
+267 -33
View File
@@ -1,15 +1,51 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AssistantService = void 0;
const nanoid_1 = require("nanoid");
const stage1Contracts_1 = require("../types/stage1Contracts");
const config_1 = require("../config");
const log_1 = require("../utils/log");
const answerComposer_1 = require("./answerComposer");
const assistantDataLayer_1 = require("./assistantDataLayer");
const assistantSessionLogger_1 = require("./assistantSessionLogger");
const investigationState_1 = require("./investigationState");
const retrievalResultNormalizer_1 = require("./retrievalResultNormalizer");
exports.evaluateCoverageForTests = evaluateCoverageForTests;
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
// @ts-nocheck
const nanoid_1 = __importStar(require("nanoid"));
const stage1Contracts_1 = __importStar(require("../types/stage1Contracts"));
const config_1 = __importStar(require("../config"));
const log_1 = __importStar(require("../utils/log"));
const answerComposer_1 = __importStar(require("./answerComposer"));
const assistantDataLayer_1 = __importStar(require("./assistantDataLayer"));
const assistantSessionLogger_1 = __importStar(require("./assistantSessionLogger"));
const investigationState_1 = __importStar(require("./investigationState"));
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
function retrievalSummaryForRoute(route) {
if (route === "store_canonical")
return "Canonical accounting data path selected.";
@@ -56,6 +92,25 @@ function extractExecutionState(normalized) {
};
});
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function enrichFragmentTextWithHints(fragment, text) {
const baseText = String(text ?? "").trim();
const accountHints = Array.isArray(fragment.account_hints)
? Array.from(new Set(fragment.account_hints
.map((item) => String(item ?? "").trim())
.filter((item) => item.length > 0)))
: [];
if (accountHints.length === 0) {
return baseText;
}
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
if (hasAccountInText) {
return baseText;
}
return `${baseText}, по счету ${accountHints.join(", ")}`;
}
function fragmentTextById(normalized) {
const result = new Map();
for (const item of extractFragments(normalized)) {
@@ -70,7 +125,7 @@ function fragmentTextById(normalized) {
const text = (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
"";
result.set(fragmentId, text);
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
}
return result;
}
@@ -95,13 +150,18 @@ function extractDiscardedIntentSegments(normalized) {
}
function collectDateSpans(text) {
const spans = [];
const datePattern = /\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g;
let match = null;
while ((match = datePattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
const datePatterns = [
/\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
];
for (const datePattern of datePatterns) {
let match = null;
while ((match = datePattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
@@ -111,17 +171,80 @@ function intersectsAnySpan(start, end, spans) {
function extractAccountTokens(text) {
const lower = String(text ?? "").toLowerCase();
const explicitAccounts = new Set();
const knownAccountPrefixes = 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 contextualPattern = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
let contextual = null;
while ((contextual = contextualPattern.exec(lower)) !== null) {
if (contextual[1]) {
explicitAccounts.add(contextual[1]);
const token = String(contextual[1]).trim();
const prefix = token.match(/^(\d{2})/)?.[1];
if (prefix && knownAccountPrefixes.has(prefix)) {
explicitAccounts.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];
const rightPrefix = right.match(/^(\d{2})/)?.[1];
if (leftPrefix && knownAccountPrefixes.has(leftPrefix)) {
explicitAccounts.add(left);
}
if (rightPrefix && knownAccountPrefixes.has(rightPrefix)) {
explicitAccounts.add(right);
}
}
if (explicitAccounts.size > 0) {
return Array.from(explicitAccounts);
}
const spans = collectDateSpans(lower);
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment|счет|СЃС‡\.?)/iu.test(lower);
if (!hasAccountingLexeme) {
return [];
}
const accountList = [];
const genericPattern = /\b\d{2}(?:\.\d{2})?\b/g;
let generic = null;
@@ -132,6 +255,10 @@ function extractAccountTokens(text) {
if (intersectsAnySpan(start, end, spans)) {
continue;
}
const prefix = value.match(/^(\d{2})/)?.[1];
if (!prefix || !knownAccountPrefixes.has(prefix)) {
continue;
}
accountList.push(value);
}
return Array.from(new Set(accountList));
@@ -406,12 +533,50 @@ function evaluateSubjectTokenMatch(token, corpus, executedRoutes) {
}
return { matched: corpus.includes(token), critical: false };
}
function evidenceCountForRequirement(requirementId, result) {
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
if (evidence.length === 0) {
return 0;
}
const tagged = evidence.filter((item) => {
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
}).length;
if (tagged > 0) {
return tagged;
}
if (Array.isArray(result.requirement_ids) &&
result.requirement_ids.length === 1 &&
result.requirement_ids[0] === requirementId) {
return evidence.length;
}
return 0;
}
function hasSubstantiveCoverageForRequirement(requirementId, result) {
const evidenceCount = evidenceCountForRequirement(requirementId, result);
if (evidenceCount > 0) {
return true;
}
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
if (Array.isArray(result.requirement_ids) &&
result.requirement_ids.length === 1 &&
result.requirement_ids[0] === requirementId) {
return true;
}
}
return false;
}
function evaluateCoverage(requirements, retrievalResults) {
const statusByRequirement = new Map();
for (const result of retrievalResults) {
for (const requirementId of result.requirement_ids) {
const list = statusByRequirement.get(requirementId) ?? [];
list.push(result.status);
list.push({
status: result.status,
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
});
statusByRequirement.set(requirementId, list);
}
}
@@ -419,19 +584,27 @@ function evaluateCoverage(requirements, retrievalResults) {
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
return requirement;
}
const statuses = statusByRequirement.get(requirement.requirement_id) ?? [];
if (statuses.length === 0) {
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
if (states.length === 0) {
return { ...requirement, status: "uncovered" };
}
if (statuses.includes("ok")) {
const hasAnySubstantive = states.some((item) => item.substantive);
if (!hasAnySubstantive) {
return { ...requirement, status: "uncovered" };
}
const hasOk = states.some((item) => item.status === "ok");
const hasPartial = states.some((item) => item.status === "partial");
const hasEmpty = states.some((item) => item.status === "empty");
const hasError = states.some((item) => item.status === "error");
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
return { ...requirement, status: "covered" };
}
if (statuses.includes("partial")) {
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
return { ...requirement, status: "partially_covered" };
}
if (statuses.includes("empty") && !statuses.includes("error")) {
return { ...requirement, status: "covered" };
}
return { ...requirement, status: "uncovered" };
});
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
@@ -459,6 +632,12 @@ function evaluateCoverage(requirements, retrievalResults) {
}
};
}
function evaluateCoverageForTests(requirements, retrievalResults) {
return evaluateCoverage(requirements, retrievalResults);
}
function extractSubjectTokensForTests(text) {
return extractSubjectTokens(text);
}
function checkGrounding(userMessage, requirements, coverage, retrievalResults) {
const whyIncludedSummary = summarizeUnique(retrievalResults.flatMap((item) => item.why_included));
const selectionReasonSummary = summarizeUnique(retrievalResults.flatMap((item) => item.selection_reason));
@@ -627,6 +806,11 @@ function buildAnswerStructureV11(input) {
};
}
const FOLLOWUP_ROUTE_HINTS = new Set(["store_canonical", "store_feature_risk", "hybrid_store_plus_live", "live_mcp_drilldown", "batch_refresh_then_store"]);
const FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP = {
settlements_60_62: "hybrid_store_plus_live",
vat_document_register_book: "hybrid_store_plus_live",
month_close_costs_20_44: "hybrid_store_plus_live"
};
const FOLLOWUP_BUSINESS_CONTEXT_MAX = 320;
const FOLLOWUP_SUBJECT_MAX = 160;
const FOLLOWUP_QUESTION_APPEND_MAX = 260;
@@ -669,6 +853,23 @@ function extractNormalizedPeriodLiteral(text) {
}
return null;
}
function extractFollowupAccountAnchorsLoose(text) {
const lower = String(text ?? "").toLowerCase();
const spans = collectDateSpans(lower);
const anchors = [];
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
let match = null;
while ((match = followupAccountPattern.exec(lower)) !== null) {
const value = String(match[0] ?? "").trim();
const start = match.index;
const end = start + value.length;
if (intersectsAnySpan(start, end, spans)) {
continue;
}
anchors.push(value);
}
return Array.from(new Set(anchors));
}
function hasStrongFollowupAnchors(userMessage, state) {
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
@@ -678,12 +879,13 @@ function hasStrongFollowupAnchors(userMessage, state) {
}
}
const explicitAccounts = extractAccountTokens(userMessage);
if (explicitAccounts.length > 0) {
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
if (followupAccounts.length > 0) {
const knownAccounts = new Set(state.focus.primary_accounts.map((item) => item.trim()));
if (knownAccounts.size === 0) {
return true;
}
if (explicitAccounts.some((item) => !knownAccounts.has(item))) {
if (followupAccounts.some((item) => !knownAccounts.has(item))) {
return true;
}
}
@@ -692,7 +894,10 @@ function hasStrongFollowupAnchors(userMessage, state) {
function routeFromInvestigationState(state) {
const rawDomain = compactWhitespace(state.focus.domain ?? "");
if (!rawDomain) {
return null;
const mappedFromFollowup = state.followup_context?.active_domain
? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null
: null;
return mappedFromFollowup;
}
if (FOLLOWUP_ROUTE_HINTS.has(rawDomain)) {
return rawDomain;
@@ -701,6 +906,15 @@ function routeFromInvestigationState(state) {
if (FOLLOWUP_ROUTE_HINTS.has(candidate)) {
return candidate;
}
if (Object.prototype.hasOwnProperty.call(FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP, candidate)) {
return FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[candidate];
}
}
const mappedFromFollowup = state.followup_context?.active_domain
? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null
: null;
if (mappedFromFollowup) {
return mappedFromFollowup;
}
return null;
}
@@ -752,6 +966,7 @@ function buildFollowupStateBinding(input) {
const hasExplicitExpectedRoute = Boolean(input.payloadContext?.expected_route);
const expectedRouteFromState = !context?.expected_route ? routeFromInvestigationState(input.investigationState) : null;
const periodHintFromState = !context?.period_hint ? input.investigationState.focus.period : null;
const followupContext = input.investigationState.followup_context;
if (expectedRouteFromState) {
context.expected_route = expectedRouteFromState;
}
@@ -771,6 +986,24 @@ function buildFollowupStateBinding(input) {
if (input.investigationState.focus.primary_accounts.length > 0) {
businessContextPatch.push(`focus_accounts:${input.investigationState.focus.primary_accounts.join(",")}`);
}
if (followupContext?.active_domain) {
businessContextPatch.push(`focus_domain:${followupContext.active_domain}`);
}
if ((followupContext?.active_requirement_ids?.length ?? 0) > 0) {
businessContextPatch.push(`active_requirements:${followupContext.active_requirement_ids.slice(0, 4).join(",")}`);
}
if ((followupContext?.uncovered_requirement_ids?.length ?? 0) > 0) {
businessContextPatch.push(`uncovered_requirements:${followupContext.uncovered_requirement_ids.slice(0, 4).join(",")}`);
}
if (followupContext?.last_problem_unit_id) {
businessContextPatch.push(`last_problem_unit:${followupContext.last_problem_unit_id}`);
}
if ((followupContext?.evidence_summary?.length ?? 0) > 0) {
businessContextPatch.push(`evidence_state:${followupContext.evidence_summary.slice(0, 3).join("|")}`);
}
if ((followupContext?.settlement_next_actions?.length ?? 0) > 0) {
businessContextPatch.push("settlement_focus_retained_v1");
}
if (problemContinuityAvailable) {
if (hasExplicitExpectedRoute) {
problemContinuitySkippedReason = "explicit_expected_route";
@@ -800,9 +1033,6 @@ function buildFollowupStateBinding(input) {
if (periodHintFromState && !hasPeriodLiteral(userMessage)) {
appendParts.push(`Период фокуса: ${periodHintFromState}`);
}
if (problemContinuityApplied && (problemState?.focus_problem_types.length ?? 0) > 0) {
appendParts.push(`Problem focus types: ${(problemState?.focus_problem_types ?? []).slice(0, 3).join(", ")}`);
}
const appendBlock = withCappedLength(compactWhitespace(appendParts.join("; ")), FOLLOWUP_QUESTION_APPEND_MAX);
normalizedQuestion = `${userMessage}\n${appendBlock}`.trim();
}
@@ -922,7 +1152,7 @@ class AssistantService {
reason: null
});
try {
const raw = this.dataLayer.executeRoute(planItem.route, planItem.fragment_text);
const raw = await this.dataLayer.executeRouteRuntime(planItem.route, planItem.fragment_text);
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
@@ -960,6 +1190,9 @@ class AssistantService {
}
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
const focusDomainHint = followupBinding.usage?.applied
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
: null;
const composition = (0, answerComposer_1.composeAssistantAnswer)({
userMessage,
routeSummary: normalized.route_hint_summary,
@@ -967,6 +1200,7 @@ class AssistantService {
requirements: coverageEvaluation.requirements,
coverageReport: coverageEvaluation.coverage,
groundingCheck,
focusDomainHint,
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1