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