NODEDC_1C/llm_normalizer/backend/dist/services/investigationState.js

650 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.inferP0DomainFromMessage = inferP0DomainFromMessage;
exports.cloneInvestigationState = cloneInvestigationState;
exports.createEmptyInvestigationState = createEmptyInvestigationState;
exports.updateInvestigationState = updateInvestigationState;
const stage1Contracts_1 = require("../types/stage1Contracts");
const stage2ProblemUnits_1 = require("../types/stage2ProblemUnits");
function uniqueStrings(values) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
}
function capStrings(values, max) {
return uniqueStrings(values).slice(0, max);
}
const INVESTIGATION_ACCOUNT_PREFIXES = 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"
]);
function collectDateLikeSpans(text) {
const spans = [];
const patterns = [
/\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,
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+20\d{2})?\b/giu
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectContractLikeSpans(text) {
const spans = [];
const patterns = [
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectAmountLikeSpans(text) {
const spans = [];
const patterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectPercentLikeSpans(text) {
const spans = [];
const pattern = /\b\d{1,3}(?:[.,]\d+)?\s*%/g;
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
return spans;
}
function intersectsSpan(start, end, spans) {
return spans.some((span) => start < span.end && end > span.start);
}
function hasAccountContextAround(text, start, end) {
const left = text.slice(Math.max(0, start - 32), start);
const right = text.slice(end, Math.min(text.length, end + 32));
return /(?:счет|сч\.?|account|schet|оплат|расчет|расч[её]т|аванс|зачет|зач[её]т|ндс|закрыт|провод|постав|покуп|settlement|payment|vat|close|supplier|customer)/iu.test(`${left} ${right}`);
}
function detectAccounts(text) {
const lower = String(text ?? "").toLowerCase();
const blockedSpans = [
...collectDateLikeSpans(lower),
...collectAmountLikeSpans(lower),
...collectPercentLikeSpans(lower),
...collectContractLikeSpans(lower)
];
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|расч[её]т|аванс|долг|settlement|payment|supplier|customer|ндс|vat|рбп|deferred|амортиз)/iu.test(lower);
const accounts = new Set();
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{1,2})?)/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
const token = String(contextualMatch[1] ?? "").trim();
const prefix = token.match(/^(\d{2})/)?.[1] ?? null;
if (prefix && INVESTIGATION_ACCOUNT_PREFIXES.has(prefix)) {
accounts.add(token);
}
}
const pairPattern = /\b(\d{2}\.\d{1,2})\s*\/\s*(\d{2}\.\d{1,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] ?? null;
const rightPrefix = right.match(/^(\d{2})/)?.[1] ?? null;
if (leftPrefix && INVESTIGATION_ACCOUNT_PREFIXES.has(leftPrefix)) {
accounts.add(left);
}
if (rightPrefix && INVESTIGATION_ACCOUNT_PREFIXES.has(rightPrefix)) {
accounts.add(right);
}
}
const genericPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
let genericMatch = null;
while ((genericMatch = genericPattern.exec(lower)) !== null) {
const token = String(genericMatch[0] ?? "").trim();
const start = genericMatch.index;
const end = start + token.length;
if (intersectsSpan(start, end, blockedSpans)) {
continue;
}
const prefix = token.match(/^(\d{2})/)?.[1] ?? null;
if (!prefix || !INVESTIGATION_ACCOUNT_PREFIXES.has(prefix)) {
continue;
}
if (!hasAccountingLexeme && !hasAccountContextAround(lower, start, end)) {
continue;
}
accounts.add(token);
}
return capStrings(Array.from(accounts), stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
}
function detectPeriod(text) {
const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
if (monthly)
return `${monthly[1]}-${monthly[2]}`;
const yearly = text.match(/\b(20\d{2})\b/);
if (yearly)
return yearly[1];
return null;
}
function inferP0DomainFromMessage(text) {
const messageCorpus = String(text ?? "").toLowerCase();
const accounts = detectAccounts(text);
const hasSettlementAccount = accounts.some((item) => isSettlementAccount(item));
const hasVatAccount = accounts.some((item) => isVatAccount(item));
const hasCloseAccount = accounts.some((item) => isCloseCostsAccount(item));
const hasFixedAssetAccount = accounts.some((item) => isFixedAssetAccount(item));
const hasSettlementLexical = /(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(messageCorpus);
const hasVatLexical = /(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
const hasCloseLexical = /(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
const hasExplicitFixedAssetLexical = /(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(messageCorpus);
const hasBroadMonthCloseLexical = /(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|month\s*close|period\s*close|регламентн)/i.test(messageCorpus);
// Keep settlement lane stable when 60/62 lexical/account anchors are explicit
// and there is no explicit VAT intent.
if ((hasSettlementAccount || hasSettlementLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical) {
return "settlements_60_62";
}
if ((hasCloseAccount || hasCloseLexical || hasBroadMonthCloseLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical && !hasFixedAssetAccount) {
return "month_close_costs_20_44";
}
if (hasVatAccount || hasVatLexical) {
return "vat_document_register_book";
}
if (hasFixedAssetAccount || hasExplicitFixedAssetLexical) {
return "fixed_asset_amortization";
}
if (hasCloseAccount || hasCloseLexical) {
return "month_close_costs_20_44";
}
if (hasSettlementAccount || hasSettlementLexical) {
return "settlements_60_62";
}
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 = inferP0DomainFromMessage(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;
if (routeSummary.mode === "legacy_v1") {
return routeSummary.route_hint;
}
const routes = routeSummary.decisions.map((item) => item.route).filter((route) => route !== "no_route");
const uniqueRoutes = uniqueStrings(routes);
if (uniqueRoutes.length === 0) {
return "no_route";
}
return uniqueRoutes.join(",");
}
function deriveNarrowingStatus(routeSummary, coverageReport) {
if (!routeSummary) {
return "unknown";
}
if (routeSummary.mode === "legacy_v1") {
return "not_needed";
}
if (routeSummary.fallback.type === "clarification" || coverageReport.clarification_needed_for.length > 0) {
return "needs_clarification";
}
const hasNoRoute = routeSummary.decisions.some((item) => item.route === "no_route");
if (hasNoRoute) {
return "broad_guarded";
}
return routeSummary.decisions.length > 1 ? "applied" : "not_needed";
}
function deriveQueryModeHint(routeSummary) {
if (!routeSummary) {
return "investigation_candidate";
}
if (routeSummary.mode === "legacy_v1") {
return "direct_answer";
}
return routeSummary.fallback.type === "none" ? "direct_answer" : "investigation_candidate";
}
function collectEvidenceRefs(retrievalResults) {
const refs = retrievalResults.flatMap((result) => result.evidence.map((item) => item.evidence_id));
return capStrings(refs, stage1Contracts_1.INVESTIGATION_MAX_EVIDENCE_REFS);
}
function collectOpenUncertainties(coverageReport, retrievalResults) {
const requirementNotes = [
...coverageReport.requirements_uncovered.map((item) => `uncovered:${item}`),
...coverageReport.requirements_partially_covered.map((item) => `partial:${item}`),
...coverageReport.clarification_needed_for.map((item) => `clarify:${item}`),
...coverageReport.out_of_scope_requirements.map((item) => `out_of_scope:${item}`)
];
const limitationNotes = retrievalResults.flatMap((result) => result.limitations).slice(0, 6);
return capStrings([...requirementNotes, ...limitationNotes], stage1Contracts_1.INVESTIGATION_MAX_UNCERTAINTIES);
}
function normalizeAccountPrefix(value) {
const account = String(value ?? "").trim();
if (!account) {
return null;
}
const match = account.match(/^(\d{2})/);
return match?.[1] ?? null;
}
function isSettlementAccount(value) {
const prefix = normalizeAccountPrefix(value);
return prefix === "60" || prefix === "62" || prefix === "51" || prefix === "76";
}
function isVatAccount(value) {
const prefix = normalizeAccountPrefix(value);
return prefix === "19" || prefix === "68";
}
function isFixedAssetAccount(value) {
const prefix = normalizeAccountPrefix(value);
return prefix === "01" || prefix === "02" || prefix === "08";
}
function isCloseCostsAccount(value) {
const prefix = normalizeAccountPrefix(value);
if (!prefix) {
return false;
}
const account = Number(prefix);
return (account >= 20 && account <= 44) || prefix === "97";
}
function inferFollowupActiveDomain(input) {
const messageCorpus = String(input.userMessage ?? "").toLowerCase();
const contextualCorpus = input.allowStateCarryover
? `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase()
: messageCorpus;
const hasFixedAssetLexicalSignal = /(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/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);
const hasBroadMonthCloseSignal = /(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|регламентн|month\s*close|period\s*close)/i.test(messageCorpus);
if ((input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus) ||
hasBroadMonthCloseSignal) &&
!hasFixedAssetLexicalSignal &&
!hasFixedAssetAccountSignal) {
return "month_close_costs_20_44";
}
if (hasFixedAssetLexicalSignal || hasFixedAssetAccountSignal) {
return "fixed_asset_amortization";
}
const hasSettlementSignal = input.focusAccounts.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 = input.focusAccounts.some((item) => isVatAccount(item)) ||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
if (hasVatSignal) {
return "vat_document_register_book";
}
const hasCloseSignal = input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus);
if (hasCloseSignal) {
return "month_close_costs_20_44";
}
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";
}
const routeDomain = deriveDomain(input.routeSummary);
if (routeDomain && routeDomain !== "no_route") {
return routeDomain;
}
if (input.allowStateCarryover) {
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
}
return null;
}
function collectUncoveredRequirementIds(coverageReport) {
return capStrings([
...coverageReport.requirements_uncovered,
...coverageReport.requirements_partially_covered,
...coverageReport.clarification_needed_for,
...coverageReport.out_of_scope_requirements
], stage1Contracts_1.INVESTIGATION_MAX_REQUIREMENT_LINKS);
}
function collectEvidenceSummary(retrievalResults) {
const lines = retrievalResults.map((result) => {
const requirementRef = result.requirement_ids[0] ?? result.fragment_id;
return `${requirementRef}:${result.status}:${result.route}`;
});
return capStrings(lines, 6);
}
function settlementFocusActions(activeDomain) {
if (activeDomain !== "settlements_60_62") {
return [];
}
return [
"Проверьте договор и объект расчетов по платежу.",
"Сверьте регистр расчетов и привязку платежа к закрывающему документу.",
"Проверьте зачет аванса или взаимозачет по связке 60/62."
];
}
function normalizeEntityBacklinks(values) {
const result = [];
const seen = new Set();
for (const item of values) {
const entity = String(item.entity ?? "").trim();
const id = String(item.id ?? "").trim();
if (!entity || !id) {
continue;
}
const key = `${entity}::${id}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push({
entity,
id
});
}
return result;
}
function collectProblemUnits(retrievalResults) {
return retrievalResults.flatMap((result) => result.problem_units ?? []);
}
function capProblemUnitState(state) {
return {
active_problem_units: capStrings(state.active_problem_units, stage2ProblemUnits_1.INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS),
resolved_problem_units: capStrings(state.resolved_problem_units, stage2ProblemUnits_1.INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS),
problem_unit_backlinks: state.problem_unit_backlinks
.map((item) => ({
problem_unit_id: String(item.problem_unit_id ?? "").trim(),
entity_backlinks: normalizeEntityBacklinks(item.entity_backlinks ?? [])
}))
.filter((item) => Boolean(item.problem_unit_id) && item.entity_backlinks.length > 0)
.slice(0, stage2ProblemUnits_1.INVESTIGATION_MAX_PROBLEM_UNIT_BACKLINKS),
focus_problem_types: capStrings(state.focus_problem_types.map((item) => String(item)), stage2ProblemUnits_1.INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES)
};
}
function updateProblemUnitState(previous, retrievalResults) {
const previousState = previous.problem_unit_state;
const currentProblemUnits = collectProblemUnits(retrievalResults);
const currentIds = capStrings(currentProblemUnits.map((item) => String(item.problem_unit_id ?? "")), stage2ProblemUnits_1.INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS);
const currentTypes = capStrings(currentProblemUnits.map((item) => String(item.problem_unit_type ?? "")), stage2ProblemUnits_1.INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES);
const currentBacklinksRaw = currentProblemUnits
.filter((item) => currentIds.includes(item.problem_unit_id))
.map((item) => ({
problem_unit_id: item.problem_unit_id,
entity_backlinks: normalizeEntityBacklinks(item.entity_backlinks ?? [])
}))
.filter((item) => item.entity_backlinks.length > 0);
const currentBacklinksById = new Map(currentBacklinksRaw.map((item) => [item.problem_unit_id, item.entity_backlinks]));
const previousBacklinksById = new Map((previousState?.problem_unit_backlinks ?? []).map((item) => [item.problem_unit_id, item.entity_backlinks]));
const active_problem_units = currentIds.length > 0
? currentIds
: capStrings(previousState?.active_problem_units ?? [], stage2ProblemUnits_1.INVESTIGATION_MAX_ACTIVE_PROBLEM_UNITS);
const resolved_problem_units = currentIds.length > 0
? capStrings([
...(previousState?.active_problem_units ?? []).filter((item) => !currentIds.includes(item)),
...(previousState?.resolved_problem_units ?? [])
], stage2ProblemUnits_1.INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS)
: capStrings(previousState?.resolved_problem_units ?? [], stage2ProblemUnits_1.INVESTIGATION_MAX_RESOLVED_PROBLEM_UNITS);
const problem_unit_backlinks = active_problem_units
.map((problemUnitId) => {
const entity_backlinks = normalizeEntityBacklinks(currentBacklinksById.get(problemUnitId) ?? previousBacklinksById.get(problemUnitId) ?? []);
if (entity_backlinks.length === 0) {
return null;
}
return {
problem_unit_id: problemUnitId,
entity_backlinks
};
})
.filter((item) => item !== null)
.slice(0, stage2ProblemUnits_1.INVESTIGATION_MAX_PROBLEM_UNIT_BACKLINKS);
const focus_problem_types = currentTypes.length > 0
? currentTypes
: capStrings((previousState?.focus_problem_types ?? []).map((item) => String(item)), stage2ProblemUnits_1.INVESTIGATION_MAX_FOCUS_PROBLEM_TYPES);
const nextState = capProblemUnitState({
active_problem_units,
resolved_problem_units,
problem_unit_backlinks,
focus_problem_types
});
if (nextState.active_problem_units.length === 0 &&
nextState.resolved_problem_units.length === 0 &&
nextState.problem_unit_backlinks.length === 0 &&
nextState.focus_problem_types.length === 0) {
return undefined;
}
return nextState;
}
function cloneInvestigationState(state) {
if (!state)
return null;
const cloned = {
...state,
focus: {
...state.focus,
primary_accounts: [...state.focus.primary_accounts]
},
evidence_refs: [...state.evidence_refs],
open_uncertainties: [...state.open_uncertainties],
followup_context: state.followup_context
? {
...state.followup_context,
referenced_requirement_ids: [...state.followup_context.referenced_requirement_ids],
...(state.followup_context.active_requirement_ids
? {
active_requirement_ids: [...state.followup_context.active_requirement_ids]
}
: {}),
...(state.followup_context.uncovered_requirement_ids
? {
uncovered_requirement_ids: [...state.followup_context.uncovered_requirement_ids]
}
: {}),
...(state.followup_context.settlement_next_actions
? {
settlement_next_actions: [...state.followup_context.settlement_next_actions]
}
: {}),
...(state.followup_context.evidence_summary
? {
evidence_summary: [...state.followup_context.evidence_summary]
}
: {})
}
: null
};
if (state.problem_unit_state) {
cloned.problem_unit_state = capProblemUnitState({
active_problem_units: [...state.problem_unit_state.active_problem_units],
resolved_problem_units: [...state.problem_unit_state.resolved_problem_units],
problem_unit_backlinks: state.problem_unit_state.problem_unit_backlinks.map((item) => ({
problem_unit_id: item.problem_unit_id,
entity_backlinks: [...item.entity_backlinks]
})),
focus_problem_types: [...state.problem_unit_state.focus_problem_types]
});
}
return cloned;
}
function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOString()) {
return {
schema_version: stage1Contracts_1.INVESTIGATION_STATE_SCHEMA_VERSION,
session_id: sessionId,
status: "idle",
turn_index: 0,
updated_at: timestamp,
question_id: null,
question_scope_id: null,
scope_origin: null,
focus: {
domain: null,
period: null,
primary_accounts: [],
active_query_subject: null
},
narrowing_status: "unknown",
evidence_refs: [],
open_uncertainties: [],
last_answer_mode: null,
followup_context: null,
query_mode_hint: "direct_answer"
};
}
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 = 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,
allowStateCarryover: followupApplied
});
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,
status: "active",
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: focusPeriod,
primary_accounts: mergedFocusAccounts,
active_query_subject: mainRequirement.slice(0, 180)
},
narrowing_status: deriveNarrowingStatus(input.routeSummary, input.coverageReport),
evidence_refs: capStrings([...collectEvidenceRefs(input.retrievalResults), ...previous.evidence_refs], stage1Contracts_1.INVESTIGATION_MAX_EVIDENCE_REFS),
open_uncertainties: collectOpenUncertainties(input.coverageReport, input.retrievalResults),
last_answer_mode: input.replyType,
followup_context: {
previous_question_id: previous.question_id,
last_user_message: input.userMessage.slice(0, 240),
referenced_requirement_ids: requirementIds,
active_domain: activeDomain,
active_requirement_ids: requirementIds,
uncovered_requirement_ids: uncoveredRequirementIds,
last_problem_unit_id: lastProblemUnitId,
settlement_next_actions: settlementNextActions,
evidence_summary: evidenceSummary,
question_scope_id: questionScopeId,
scope_origin: scopeOrigin
},
query_mode_hint: deriveQueryModeHint(input.routeSummary),
...(problemUnitState
? {
problem_unit_state: problemUnitState
}
: {})
};
}