Этап 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
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