Удержать контекст СВК в follow-up ассистента

This commit is contained in:
dctouch 2026-06-15 22:56:10 +03:00
parent 7e70ec1d46
commit b3f2e405bb
23 changed files with 950 additions and 91 deletions

View File

@ -127,6 +127,48 @@ function readNavigationDiscoveryCounterparty(debug) {
}
return null;
}
function readDiscoveryPilot(debug) {
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const bridge = toObject(entry?.bridge);
return toObject(bridge?.pilot);
}
function readCurrentDiscoveryCounterparty(debug) {
const pilot = readDiscoveryPilot(debug);
const entityResolution = toObject(pilot?.derived_entity_resolution);
const bidirectionalValueFlow = toObject(pilot?.derived_bidirectional_value_flow);
const valueFlow = toObject(pilot?.derived_value_flow);
return (toNonEmptyString(entityResolution?.resolved_entity) ??
toNonEmptyString(bidirectionalValueFlow?.counterparty) ??
toNonEmptyString(valueFlow?.counterparty) ??
(0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ??
readNavigationDiscoveryCounterparty(debug));
}
function isCounterpartyDiscoveryChain(value) {
return (value === "entity_resolution" ||
value === "value_flow" ||
value === "value_flow_comparison" ||
value === "document_evidence" ||
value === "movement_evidence" ||
value === "lifecycle");
}
function intentFromDiscoveryChain(value) {
if (value === "business_overview") {
return "business_overview";
}
if (value === "value_flow" || value === "value_flow_comparison") {
return "customer_revenue_and_payments";
}
if (value === "document_evidence") {
return "list_documents_by_counterparty";
}
if (value === "movement_evidence") {
return "bank_operations_by_counterparty";
}
if (value === "lifecycle") {
return "counterparty_activity_lifecycle";
}
return "unknown";
}
function toAddressFocusObjectType(value) {
const normalized = toNonEmptyString(value);
if (!normalized) {
@ -364,8 +406,7 @@ function readBusinessOverviewComparisonProofBundles(debug) {
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const pilot = readDiscoveryPilot(debug);
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
@ -383,6 +424,18 @@ function readBusinessOverviewComparisonProofBundles(debug) {
counterparty_document_bundle: counterpartyDocumentBundle
};
}
function readCounterpartyValueFlowProofBundles(debug) {
const pilot = readDiscoveryPilot(debug);
const counterpartyValueFlowBundle = cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty);
if (!counterparty || !counterpartyValueFlowBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: null
};
}
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
@ -396,8 +449,8 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
const counterparty = readCurrentDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
@ -587,14 +640,13 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
}
const debug = item.debug;
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
const discoveryIntent = selectedDiscoveryChain === "business_overview"
? "business_overview"
: selectedDiscoveryChain === "value_flow_comparison"
? "customer_revenue_and_payments"
: "unknown";
const detectedIntent = toNonEmptyString(debug.detected_intent);
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
const shouldPreferDiscoveryIntent = trackableDiscoveryTurn &&
discoveryIntent !== "unknown" &&
selectedDiscoveryChain !== "business_overview";
const intent = toAddressIntent(shouldPreferDiscoveryIntent ? discoveryIntent : detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
if (intent === "unknown" && !trackableDiscoveryTurn) {
return state;
}
@ -603,8 +655,8 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
const derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
const derivedCounterpartyScope = isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
? readCurrentDiscoveryCounterparty(debug)
: null;
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
? {
@ -637,8 +689,12 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
const rawComparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
const rawComparisonCounterparty = debug.mcp_discovery_response_applied === true
? selectedDiscoveryChain === "business_overview"
? readNavigationDiscoveryCounterparty(debug)
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
? readCurrentDiscoveryCounterparty(debug)
: null
: null;
const comparisonCounterparty = rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
? rawComparisonCounterparty
@ -648,6 +704,8 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
: null;
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug)
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readCounterpartyValueFlowProofBundles(debug)
: null;
const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles = comparisonCounterparty &&

View File

@ -1313,6 +1313,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
const currentPeriodFromForCarryover = toNonEmptyString(merged.period_from);
const currentPeriodToForCarryover = toNonEmptyString(merged.period_to);
const todayIsoForPeriodCarryover = new Date().toISOString().slice(0, 10);
const currentValueFlowPeriodDefaultsToToday = isValueCounterpartyIntent(intent) &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!hasExplicitCurrentDateInMessage &&
!currentPeriodFromForCarryover &&
currentPeriodToForCarryover === todayIsoForPeriodCarryover;
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
const currentContractExplicit = toNonEmptyString(merged.contract);
const currentItemExplicit = toNonEmptyString(merged.item);
@ -1387,7 +1396,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
}
reasons.push("period_from_followup_context");
}
if (!currentHasPeriod &&
if ((!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
previousHasPeriod &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&

View File

@ -778,7 +778,11 @@ function resolveFollowupTargetIntent(inventoryPurchaseDateVatBridge, selectedObj
}
function shouldUseNavigationTemporalCarryover(sourceIntentHint) {
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
return (normalizedIntent === "inventory_on_hand_as_of_date" ||
return (normalizedIntent === "customer_revenue_and_payments" ||
normalizedIntent === "supplier_payouts_profile" ||
normalizedIntent === "contract_usage_and_value" ||
normalizedIntent === "counterparty_activity_lifecycle" ||
normalizedIntent === "inventory_on_hand_as_of_date" ||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
normalizedIntent === "inventory_purchase_documents_for_item" ||

View File

@ -212,7 +212,7 @@ function valueFlowDirectionLabelRu(pilot) {
? "исходящих платежей/списаний"
: "входящих денежных поступлений";
}
function valueFlowZeroResultConfirmedLine(pilot) {
function valueFlowZeroResultInsufficiencyLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) {
return null;
}
@ -224,7 +224,22 @@ function valueFlowZeroResultConfirmedLine(pilot) {
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
const direction = valueFlowDirectionLabelRu(pilot);
return `Точную сумму ${direction} по контрагенту ${counterparty}${organizationPart}${periodPart} не подтверждаю: в выполненной проверке 1С строки ${direction} не найдены; это не доказывает отсутствие операций вне доступного банковского контура.`;
}
function valueFlowZeroResultCheckedLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) {
return null;
}
const counterparty = firstEntityCandidate(pilot);
if (!counterparty) {
return null;
}
const organization = explicitOrganizationScope(pilot);
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} строки ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
}
function valueFlowZeroResultUnknownLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) {
@ -239,11 +254,11 @@ function valueFlowZeroResultUnknownLine(pilot) {
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
}
function valueFlowZeroResultHeadline(pilot) {
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
if (!confirmedLine) {
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
if (!insufficiencyLine) {
return null;
}
return confirmedLine;
return insufficiencyLine;
}
function hasAllTimeScope(pilot) {
return (dryRunHasAxis(pilot, "all_time_scope") ||
@ -1846,8 +1861,6 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
? [derivedValueLine]
: derivedValueLine
? [derivedValueLine, ...monthlyConfirmedLines]
: valueFlowZeroResultConfirmedLine(pilot)
? [valueFlowZeroResultConfirmedLine(pilot)]
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
@ -1859,7 +1872,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
: appendValueFlowZeroResultUnknown(valueFlowZeroResultCheckedLine(pilot)
? [valueFlowZeroResultCheckedLine(pilot), ...rankedValueFlowUnknownLines(pilot)]
: rankedValueFlowUnknownLines(pilot), pilot);
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",

View File

@ -2209,9 +2209,28 @@ function deriveBusinessOverviewContractUsageProfile(result, periodScope) {
};
}
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
if (!result || result.error || result.matched_rows <= 0) {
if (!result || result.error) {
return null;
}
if (result.matched_rows <= 0) {
return {
value_flow_direction: direction,
counterparty,
period_scope: periodScope,
aggregation_axis: aggregationAxis,
rows_matched: 0,
rows_with_amount: 0,
total_amount: 0,
total_amount_human_ru: formatAmountHumanRu(0),
first_movement_date: null,
latest_movement_date: null,
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
period_chunking_granularity: result.period_chunking_granularity,
monthly_breakdown: [],
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
@ -3926,16 +3945,30 @@ function buildMovementConfirmedFacts(result, counterparty, periodScope) {
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
}
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
if (result.error || result.matched_rows <= 0) {
if (result.error) {
return [];
}
if (direction === "outgoing_supplier_payout") {
if (result.matched_rows <= 0) {
return [
counterparty
? `1C supplier-payout rows were checked for counterparty ${counterparty}: not_found`
: "1C supplier-payout rows were checked for the requested counterparty scope: not_found"
];
}
return [
counterparty
? `1C supplier-payout rows were found for counterparty ${counterparty}`
: "1C supplier-payout rows were found for the requested counterparty scope"
];
}
if (result.matched_rows <= 0) {
return [
counterparty
? `1C value-flow rows were checked for counterparty ${counterparty}: not_found`
: "1C value-flow rows were checked for the requested counterparty scope: not_found"
];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
@ -4115,6 +4148,12 @@ function buildValueFlowInferredFacts(derived) {
return [];
}
const facts = [];
if (derived.rows_matched <= 0) {
facts.push(derived.value_flow_direction === "outgoing_supplier_payout"
? "Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set"
: "Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set");
return facts;
}
if (derived.value_flow_direction === "outgoing_supplier_payout") {
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
}

View File

@ -261,13 +261,14 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
const rowsMatched = probeRowsMatched(probeResults);
const rowsReceived = probeRowsReceived(probeResults);
const bypassDetected = hasProbeBypass(input.plan, probeResults);
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
if (bypassDetected) {
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
}
if (input.plan.plan_status !== "allowed") {
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
}
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
if (confirmedFacts.length > 0 && rowsMatched <= 0 && !confirmedNoMatchFact) {
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
}
if (!sourceRowsSummary && rowsReceived > 0) {
@ -287,6 +288,12 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
answerPermission = "confirmed_answer";
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
}
else if (confirmedFacts.length > 0 && confirmedNoMatchFact && rowsMatched <= 0 && sourceRowsSummary) {
evidenceStatus = "confirmed";
coverageStatus = "full";
answerPermission = "confirmed_answer";
pushReason(reasonCodes, "confirmed_no_match_fact_with_allowed_mcp_evidence");
}
else if (inferredFacts.length > 0 && rowsReceived > 0) {
evidenceStatus = "inferred_only";
coverageStatus = "partial";

View File

@ -211,9 +211,15 @@ function localizeLine(value) {
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
}
if (/^1C value-flow rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
return "В 1С проверены входящие денежные строки в запрошенном срезе: строки не найдены.";
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
}
if (/^1C supplier-payout rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
return "В 1С проверены исходящие платежи/списания в запрошенном срезе: строки не найдены.";
}
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
if (openScopeBidirectionalMatch) {
const incoming = openScopeBidirectionalMatch[1] === "found"
@ -238,6 +244,10 @@ function localizeLine(value) {
if (valueFlowMatch) {
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
}
const valueFlowNotFoundMatch = value.match(/^1C value-flow rows were checked for counterparty\s+(.+): not_found$/i);
if (valueFlowNotFoundMatch) {
return `В 1С проверены входящие денежные строки по контрагенту ${valueFlowNotFoundMatch[1]}: строки не найдены.`;
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
}
@ -259,6 +269,10 @@ function localizeLine(value) {
if (supplierPayoutMatch) {
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
}
const supplierPayoutNotFoundMatch = value.match(/^1C supplier-payout rows were checked for counterparty\s+(.+): not_found$/i);
if (supplierPayoutNotFoundMatch) {
return `В 1С проверены исходящие платежи/списания по контрагенту ${supplierPayoutNotFoundMatch[1]}: строки не найдены.`;
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
}
@ -292,6 +306,12 @@ function localizeLine(value) {
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
}
if (/^Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
return "Нулевую сумму исходящих платежей можно трактовать только внутри этого проверенного среза 1С, где строки списаний не найдены.";
}
if (/^Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
return "Нулевую сумму входящих поступлений можно трактовать только внутри этого проверенного среза 1С, где строки поступлений не найдены.";
}
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
}
@ -1159,7 +1179,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
].filter((value) => Boolean(value));
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`);
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`);
lines.push("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
lines.push("Что видно в ограниченном денежном срезе:");
if (incomingAmount) {
@ -1194,7 +1214,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push(`- ${localizeLine(debtLine)}`);
}
if (!limitLine) {
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли.");
}
const missingOverviewFamilies = [];
if (!taxLine) {

View File

@ -615,6 +615,53 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
routeMode === "exact" ||
hasFullConfirmedTruth(input));
}
function extractExplicitDateScopeYear(value) {
const source = toNonEmptyString(value);
if (!source) {
return null;
}
const match = source.match(/\b((?:19|20)\d{2})\b/u);
return match ? match[1] : null;
}
function extractDocumentReplyDateYears(value) {
const source = String(value ?? "");
if (!source) {
return [];
}
const years = new Set();
for (const match of source.matchAll(/\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)/gu)) {
years.add(match[1]);
}
for (const match of source.matchAll(/\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b/gu)) {
years.add(match[1]);
}
return [...years];
}
function hasDocumentListTemporalConflictWithDiscovery(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const isDocumentIntent = detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
const isDocumentRecipe = selectedRecipe === "address_documents_by_counterparty_v1" ||
selectedRecipe === "address_documents_by_contract_v1";
if (!isDocumentIntent || !isDocumentRecipe) {
return false;
}
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const expectedYear = extractExplicitDateScopeYear(turnMeaning?.explicit_date_scope);
if (!expectedYear) {
return false;
}
const filters = toRecordObject(input.addressRuntimeMeta?.extracted_filters);
const periodFrom = toNonEmptyString(filters?.period_from);
const periodTo = toNonEmptyString(filters?.period_to);
const hasMatchingPeriod = Boolean(periodFrom?.startsWith(`${expectedYear}-`)) && Boolean(periodTo?.startsWith(`${expectedYear}-`));
const replyDateYears = extractDocumentReplyDateYears(input.currentReply);
const hasOutOfScopeReplyDates = replyDateYears.some((year) => year !== expectedYear);
return hasOutOfScopeReplyDates || (!hasMatchingPeriod && replyDateYears.length > 0);
}
function hasExactDocumentListAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
@ -968,7 +1015,8 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint);
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
@ -987,10 +1035,12 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!groundedMovementCandidatePriority &&
!businessOverviewBoundaryCandidatePriority &&
!groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget &&
!groundedValueFlowRankingCandidatePriority &&
!documentListTemporalConflictWithDiscovery;
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning &&
!valueFlowActionConflictWithDiscoveryTurnMeaning;
@ -1024,6 +1074,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
}
if (documentListTemporalConflictWithDiscovery) {
pushReason(reasonCodes, "mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override");
}
if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
}

View File

@ -151,6 +151,56 @@ function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): st
return null;
}
function readDiscoveryPilot(debug: Record<string, unknown>): Record<string, unknown> | null {
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const bridge = toObject(entry?.bridge);
return toObject(bridge?.pilot);
}
function readCurrentDiscoveryCounterparty(debug: Record<string, unknown>): string | null {
const pilot = readDiscoveryPilot(debug);
const entityResolution = toObject(pilot?.derived_entity_resolution);
const bidirectionalValueFlow = toObject(pilot?.derived_bidirectional_value_flow);
const valueFlow = toObject(pilot?.derived_value_flow);
return (
toNonEmptyString(entityResolution?.resolved_entity) ??
toNonEmptyString(bidirectionalValueFlow?.counterparty) ??
toNonEmptyString(valueFlow?.counterparty) ??
readAddressDebugCounterparty(debug, toNonEmptyString) ??
readNavigationDiscoveryCounterparty(debug)
);
}
function isCounterpartyDiscoveryChain(value: string | null): boolean {
return (
value === "entity_resolution" ||
value === "value_flow" ||
value === "value_flow_comparison" ||
value === "document_evidence" ||
value === "movement_evidence" ||
value === "lifecycle"
);
}
function intentFromDiscoveryChain(value: string | null): AddressIntent {
if (value === "business_overview") {
return "business_overview";
}
if (value === "value_flow" || value === "value_flow_comparison") {
return "customer_revenue_and_payments";
}
if (value === "document_evidence") {
return "list_documents_by_counterparty";
}
if (value === "movement_evidence") {
return "bank_operations_by_counterparty";
}
if (value === "lifecycle") {
return "counterparty_activity_lifecycle";
}
return "unknown";
}
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
const normalized = toNonEmptyString(value);
if (!normalized) {
@ -437,8 +487,7 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const pilot = readDiscoveryPilot(debug);
const counterpartyValueFlowBundle =
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow);
@ -459,6 +508,19 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
};
}
function readCounterpartyValueFlowProofBundles(debug: Record<string, unknown>): AddressComparisonProofBundles | null {
const pilot = readDiscoveryPilot(debug);
const counterpartyValueFlowBundle = cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty);
if (!counterparty || !counterpartyValueFlowBundle) {
return null;
}
return {
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
counterparty_document_bundle: null
};
}
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
@ -472,9 +534,8 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
return buildFocusObject("organization", organization, resultSetId, createdAt);
}
}
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty =
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
const counterparty = readCurrentDiscoveryCounterparty(debug);
if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
}
@ -697,15 +758,16 @@ export function evolveAddressNavigationStateWithAssistantItem(
}
const debug = item.debug as unknown as Record<string, unknown>;
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
const discoveryIntent =
selectedDiscoveryChain === "business_overview"
? "business_overview"
: selectedDiscoveryChain === "value_flow_comparison"
? "customer_revenue_and_payments"
: "unknown";
const detectedIntent = toNonEmptyString(debug.detected_intent);
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
const shouldPreferDiscoveryIntent =
trackableDiscoveryTurn &&
discoveryIntent !== "unknown" &&
selectedDiscoveryChain !== "business_overview";
const intent = toAddressIntent(
shouldPreferDiscoveryIntent ? discoveryIntent : detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent
);
if (intent === "unknown" && !trackableDiscoveryTurn) {
return state;
}
@ -716,8 +778,8 @@ export function evolveAddressNavigationStateWithAssistantItem(
const derivedOrganizationScope =
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
const derivedCounterpartyScope =
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
? readCurrentDiscoveryCounterparty(debug)
: null;
const filtersWithDerivedScope =
derivedOrganizationScope && !toNonEmptyString(filters.organization)
@ -758,8 +820,12 @@ export function evolveAddressNavigationStateWithAssistantItem(
primaryEntityFocusObject
);
const rawComparisonCounterparty =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
debug.mcp_discovery_response_applied === true
? selectedDiscoveryChain === "business_overview"
? readNavigationDiscoveryCounterparty(debug)
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
? readCurrentDiscoveryCounterparty(debug)
: null
: null;
const comparisonCounterparty =
rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
@ -772,6 +838,8 @@ export function evolveAddressNavigationStateWithAssistantItem(
const currentComparisonProofBundles =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug)
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readCounterpartyValueFlowProofBundles(debug)
: null;
const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles =

View File

@ -1629,6 +1629,16 @@ function mergeFollowupFilters(
intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
const currentPeriodFromForCarryover = toNonEmptyString(merged.period_from);
const currentPeriodToForCarryover = toNonEmptyString(merged.period_to);
const todayIsoForPeriodCarryover = new Date().toISOString().slice(0, 10);
const currentValueFlowPeriodDefaultsToToday =
isValueCounterpartyIntent(intent) &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!hasExplicitCurrentDateInMessage &&
!currentPeriodFromForCarryover &&
currentPeriodToForCarryover === todayIsoForPeriodCarryover;
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
const currentContractExplicit = toNonEmptyString(merged.contract);
const currentItemExplicit = toNonEmptyString(merged.item);
@ -1717,7 +1727,7 @@ function mergeFollowupFilters(
}
if (
!currentHasPeriod &&
(!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
previousHasPeriod &&
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&

View File

@ -1158,6 +1158,10 @@ export function resolveFollowupTargetIntent(
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
return (
normalizedIntent === "customer_revenue_and_payments" ||
normalizedIntent === "supplier_payouts_profile" ||
normalizedIntent === "contract_usage_and_value" ||
normalizedIntent === "counterparty_activity_lifecycle" ||
normalizedIntent === "inventory_on_hand_as_of_date" ||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
normalizedIntent === "inventory_purchase_provenance_for_item" ||

View File

@ -281,7 +281,7 @@ function valueFlowDirectionLabelRu(pilot: AssistantMcpDiscoveryPilotExecutionCon
: "входящих денежных поступлений";
}
function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
function valueFlowZeroResultInsufficiencyLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (!hasExecutedZeroValueFlowRows(pilot)) {
return null;
}
@ -293,7 +293,23 @@ function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecu
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(
const direction = valueFlowDirectionLabelRu(pilot);
return `Точную сумму ${direction} по контрагенту ${counterparty}${organizationPart}${periodPart} не подтверждаю: в выполненной проверке 1С строки ${direction} не найдены; это не доказывает отсутствие операций вне доступного банковского контура.`;
}
function valueFlowZeroResultCheckedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (!hasExecutedZeroValueFlowRows(pilot)) {
return null;
}
const counterparty = firstEntityCandidate(pilot);
if (!counterparty) {
return null;
}
const organization = explicitOrganizationScope(pilot);
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} строки ${valueFlowDirectionLabelRu(
pilot
)} не найдено.`;
}
@ -312,11 +328,11 @@ function valueFlowZeroResultUnknownLine(pilot: AssistantMcpDiscoveryPilotExecuti
}
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
if (!confirmedLine) {
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
if (!insufficiencyLine) {
return null;
}
return confirmedLine;
return insufficiencyLine;
}
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
@ -2126,8 +2142,6 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
? [derivedValueLine]
: derivedValueLine
? [derivedValueLine, ...monthlyConfirmedLines]
: valueFlowZeroResultConfirmedLine(pilot)
? [valueFlowZeroResultConfirmedLine(pilot)!]
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
@ -2139,7 +2153,12 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
: appendValueFlowZeroResultUnknown(
valueFlowZeroResultCheckedLine(pilot)
? [valueFlowZeroResultCheckedLine(pilot)!, ...rankedValueFlowUnknownLines(pilot)]
: rankedValueFlowUnknownLines(pilot),
pilot
);
return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,

View File

@ -3217,9 +3217,28 @@ function deriveValueFlow(
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
): AssistantMcpDiscoveryDerivedValueFlow | null {
if (!result || result.error || result.matched_rows <= 0) {
if (!result || result.error) {
return null;
}
if (result.matched_rows <= 0) {
return {
value_flow_direction: direction,
counterparty,
period_scope: periodScope,
aggregation_axis: aggregationAxis,
rows_matched: 0,
rows_with_amount: 0,
total_amount: 0,
total_amount_human_ru: formatAmountHumanRu(0),
first_movement_date: null,
latest_movement_date: null,
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
period_chunking_granularity: result.period_chunking_granularity,
monthly_breakdown: [],
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
@ -5294,16 +5313,30 @@ function buildValueFlowConfirmedFacts(
counterparty: string | null,
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
): string[] {
if (result.error || result.matched_rows <= 0) {
if (result.error) {
return [];
}
if (direction === "outgoing_supplier_payout") {
if (result.matched_rows <= 0) {
return [
counterparty
? `1C supplier-payout rows were checked for counterparty ${counterparty}: not_found`
: "1C supplier-payout rows were checked for the requested counterparty scope: not_found"
];
}
return [
counterparty
? `1C supplier-payout rows were found for counterparty ${counterparty}`
: "1C supplier-payout rows were found for the requested counterparty scope"
];
}
if (result.matched_rows <= 0) {
return [
counterparty
? `1C value-flow rows were checked for counterparty ${counterparty}: not_found`
: "1C value-flow rows were checked for the requested counterparty scope: not_found"
];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
@ -5521,6 +5554,14 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
return [];
}
const facts: string[] = [];
if (derived.rows_matched <= 0) {
facts.push(
derived.value_flow_direction === "outgoing_supplier_payout"
? "Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set"
: "Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set"
);
return facts;
}
if (derived.value_flow_direction === "outgoing_supplier_payout") {
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
} else {

View File

@ -386,6 +386,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
const rowsMatched = probeRowsMatched(probeResults);
const rowsReceived = probeRowsReceived(probeResults);
const bypassDetected = hasProbeBypass(input.plan, probeResults);
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
if (bypassDetected) {
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
@ -393,7 +394,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
if (input.plan.plan_status !== "allowed") {
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
}
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
if (confirmedFacts.length > 0 && rowsMatched <= 0 && !confirmedNoMatchFact) {
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
}
if (!sourceRowsSummary && rowsReceived > 0) {
@ -413,6 +414,11 @@ export function resolveAssistantMcpDiscoveryEvidence(
coverageStatus = "full";
answerPermission = "confirmed_answer";
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
} else if (confirmedFacts.length > 0 && confirmedNoMatchFact && rowsMatched <= 0 && sourceRowsSummary) {
evidenceStatus = "confirmed";
coverageStatus = "full";
answerPermission = "confirmed_answer";
pushReason(reasonCodes, "confirmed_no_match_fact_with_allowed_mcp_evidence");
} else if (inferredFacts.length > 0 && rowsReceived > 0) {
evidenceStatus = "inferred_only";
coverageStatus = "partial";

View File

@ -284,9 +284,15 @@ function localizeLine(value: string): string {
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
}
if (/^1C value-flow rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
return "В 1С проверены входящие денежные строки в запрошенном срезе: строки не найдены.";
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
}
if (/^1C supplier-payout rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
return "В 1С проверены исходящие платежи/списания в запрошенном срезе: строки не найдены.";
}
const openScopeBidirectionalMatch = value.match(
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
);
@ -319,6 +325,10 @@ function localizeLine(value: string): string {
if (valueFlowMatch) {
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
}
const valueFlowNotFoundMatch = value.match(/^1C value-flow rows were checked for counterparty\s+(.+): not_found$/i);
if (valueFlowNotFoundMatch) {
return `В 1С проверены входящие денежные строки по контрагенту ${valueFlowNotFoundMatch[1]}: строки не найдены.`;
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
}
@ -340,6 +350,10 @@ function localizeLine(value: string): string {
if (supplierPayoutMatch) {
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
}
const supplierPayoutNotFoundMatch = value.match(/^1C supplier-payout rows were checked for counterparty\s+(.+): not_found$/i);
if (supplierPayoutNotFoundMatch) {
return `В 1С проверены исходящие платежи/списания по контрагенту ${supplierPayoutNotFoundMatch[1]}: строки не найдены.`;
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
}
@ -377,6 +391,12 @@ function localizeLine(value: string): string {
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
}
if (/^Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
return "Нулевую сумму исходящих платежей можно трактовать только внутри этого проверенного среза 1С, где строки списаний не найдены.";
}
if (/^Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
return "Нулевую сумму входящих поступлений можно трактовать только внутри этого проверенного среза 1С, где строки поступлений не найдены.";
}
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
}
@ -1429,7 +1449,7 @@ function buildCompactBusinessOverviewReply(
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
].filter((value): value is string => Boolean(value));
lines.push(
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`
);
lines.push(
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
@ -1472,7 +1492,7 @@ function buildCompactBusinessOverviewReply(
}
if (!limitLine) {
lines.push(
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли."
);
}
const missingOverviewFamilies: string[] = [];

View File

@ -855,6 +855,65 @@ function hasExactBankOperationsAddressReply(
);
}
function extractExplicitDateScopeYear(value: unknown): string | null {
const source = toNonEmptyString(value);
if (!source) {
return null;
}
const match = source.match(/\b((?:19|20)\d{2})\b/u);
return match ? match[1] : null;
}
function extractDocumentReplyDateYears(value: unknown): string[] {
const source = String(value ?? "");
if (!source) {
return [];
}
const years = new Set<string>();
for (const match of source.matchAll(/\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)/gu)) {
years.add(match[1]);
}
for (const match of source.matchAll(/\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b/gu)) {
years.add(match[1]);
}
return [...years];
}
function hasDocumentListTemporalConflictWithDiscovery(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const isDocumentIntent =
detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
const isDocumentRecipe =
selectedRecipe === "address_documents_by_counterparty_v1" ||
selectedRecipe === "address_documents_by_contract_v1";
if (!isDocumentIntent || !isDocumentRecipe) {
return false;
}
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const expectedYear = extractExplicitDateScopeYear(turnMeaning?.explicit_date_scope);
if (!expectedYear) {
return false;
}
const filters = toRecordObject(input.addressRuntimeMeta?.extracted_filters);
const periodFrom = toNonEmptyString(filters?.period_from);
const periodTo = toNonEmptyString(filters?.period_to);
const hasMatchingPeriod =
Boolean(periodFrom?.startsWith(`${expectedYear}-`)) && Boolean(periodTo?.startsWith(`${expectedYear}-`));
const replyDateYears = extractDocumentReplyDateYears(input.currentReply);
const hasOutOfScopeReplyDates = replyDateYears.some((year) => year !== expectedYear);
return hasOutOfScopeReplyDates || (!hasMatchingPeriod && replyDateYears.length > 0);
}
function hasExactDocumentListAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@ -1299,7 +1358,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
entryPoint
);
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint);
const exactDocumentListAddressReply =
hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
input,
@ -1340,13 +1401,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!businessOverviewBoundaryCandidatePriority &&
!groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent =
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const matchedFactualAddressContinuationTargetProtectsCurrent =
matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
matchedFactualAddressContinuationTarget &&
!groundedValueFlowRankingCandidatePriority &&
!documentListTemporalConflictWithDiscovery;
const fullConfirmedFactualAddressReplyProtectsCurrent =
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const exactMatchedFactualAddressReplyProtectsCurrent =
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const exactBankOperationsProtectsCurrent =
exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning &&
@ -1383,6 +1446,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
}
if (documentListTemporalConflictWithDiscovery) {
pushReason(reasonCodes, "mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override");
}
if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
}

View File

@ -164,6 +164,28 @@ describe("address follow-up temporal regressions", () => {
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
});
it("keeps explicit year period for selected counterparty payout follow-up", () => {
const result = runAddressDecomposeStage("а теперь сколько заплатили?", {
previous_intent: "customer_revenue_and_payments" as const,
target_intent: "customer_revenue_and_payments" as const,
previous_filters: {
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty" as const,
previous_anchor_value: "Группа СВК",
resolved_counterparty_from_display: true
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("customer_revenue_and_payments");
expect(result?.filters.extracted_filters.counterparty).toBe("Группа СВК");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
expect(result?.baseReasons).toContain("period_from_followup_context");
});
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
previous_intent: "vat_payable_confirmed_as_of_date",
@ -454,6 +476,29 @@ describe("address follow-up temporal regressions", () => {
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
});
it("keeps value-flow period for counterparty document evidence follow-up", () => {
const followupContext = {
previous_intent: "customer_revenue_and_payments" as const,
target_intent: "list_documents_by_counterparty" as const,
previous_filters: {
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty" as const,
previous_anchor_value: "Группа СВК",
resolved_counterparty_from_display: true
};
const documents = runAddressDecomposeStage("а по документам?", followupContext);
expect(documents?.intent.intent).toBe("list_documents_by_counterparty");
expect(documents?.filters.extracted_filters.counterparty).toBe("Группа СВК");
expect(documents?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(documents?.filters.extracted_filters.period_to).toBe("2020-12-31");
expect(documents?.baseReasons).toContain("period_from_followup_context");
});
it("replaces pronoun chain anchors from counterparty follow-up context", () => {
const followupContext = {
previous_intent: "customer_revenue_and_payments" as const,

View File

@ -220,6 +220,262 @@ describe("address navigation state", () => {
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("captures resolved entity-resolution focus as fresh current discovery state", () => {
const base = createEmptyAddressNavigationState("asst-entity", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-entity",
session_id: "asst-entity",
role: "assistant",
text: "В проверенном каталожном срезе 1С найден наиболее вероятный контрагент: Группа СВК.",
created_at: "2026-04-12T10:12:30.000Z",
debug: {
detected_mode: "chat",
detected_intent: "unknown",
extracted_filters: {},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "entity_resolution",
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"],
stale_replay_forbidden: true
}
},
bridge: {
pilot: {
pilot_scope: "entity_resolution_search_v1",
derived_entity_resolution: {
requested_entity: "СВК",
resolution_status: "resolved",
resolved_entity: "Группа СВК"
}
}
}
},
dialog_continuation_contract_v2: {
decision: "new_topic"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 1);
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(evolved.result_sets[0]?.filters.counterparty).toBe("Группа СВК");
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
});
it("captures single-direction value-flow focus before the net bundle exists", () => {
const base = createEmptyAddressNavigationState("asst-vf-single", "2026-04-12T10:00:00.000Z");
const assistantItem = {
message_id: "msg-vf-single",
session_id: "asst-vf-single",
role: "assistant",
text: "Входящие денежные поступления по контрагенту Группа СВК за период 2020: 12 093 465 руб.",
created_at: "2026-04-12T10:13:00.000Z",
debug: {
detected_mode: "chat",
detected_intent: "unknown",
extracted_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "value_flow",
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
stale_replay_forbidden: true
}
},
bridge: {
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1",
derived_value_flow: {
value_flow_direction: "incoming_customer_revenue",
counterparty: "Группа СВК",
period_scope: "2020",
total_amount_human_ru: "12 093 465 руб."
}
}
}
},
dialog_continuation_contract_v2: {
decision: "continue_previous"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
expect(evolved.result_sets[0]?.intent).toBe("customer_revenue_and_payments");
expect(evolved.result_sets[0]?.filters.counterparty).toBe("Группа СВК");
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
});
it("stores bidirectional value-flow proof bundle for adjacent evidence drilldowns", () => {
const base = createEmptyAddressNavigationState("asst-vf-bundle", "2026-04-12T10:00:00.000Z");
const valueFlowBundle = {
counterparty: "Группа СВК",
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "12 093 465 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "0 руб."
},
net_amount_human_ru: "12 093 465 руб.",
net_direction: "net_incoming"
};
const netItem = {
message_id: "msg-vf-net",
session_id: "asst-vf-bundle",
role: "assistant",
text: "По контрагенту Группа СВК за период 2020 получили 12 093 465 руб., заплатили 0 руб.; расчетное нетто в нашу сторону: 12 093 465 руб.",
created_at: "2026-04-12T10:14:00.000Z",
debug: {
detected_mode: "chat",
detected_intent: "customer_revenue_and_payments",
extracted_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "value_flow_comparison",
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
stale_replay_forbidden: true
}
},
bridge: {
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: valueFlowBundle
}
}
},
dialog_continuation_contract_v2: {
decision: "continue_previous"
}
}
} as any;
const afterNet = evolveAddressNavigationStateWithAssistantItem(base, netItem, 5);
expect(afterNet.session_context.comparison_scope?.counterparty?.label).toBe("Группа СВК");
expect(afterNet.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
const docsItem = {
message_id: "msg-vf-docs",
session_id: "asst-vf-bundle",
role: "assistant",
text: "По документам по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
created_at: "2026-04-12T10:15:00.000Z",
debug: {
detected_mode: "chat",
detected_intent: "list_documents_by_counterparty",
extracted_filters: {
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "document_evidence",
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
stale_replay_forbidden: true
}
},
bridge: {
pilot: {
pilot_scope: "counterparty_document_evidence_query_documents_v1"
}
}
},
dialog_continuation_contract_v2: {
decision: "continue_previous"
}
}
} as any;
const afterDocs = evolveAddressNavigationStateWithAssistantItem(afterNet, docsItem, 6);
expect(afterDocs.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(afterDocs.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
const movementsItem = {
message_id: "msg-vf-movements",
session_id: "asst-vf-bundle",
role: "assistant",
text: "По движениям по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
created_at: "2026-04-12T10:16:00.000Z",
debug: {
detected_mode: "chat",
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "movement_evidence",
assistant_mcp_discovery_entry_point_v1: {
turn_input: {
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
stale_replay_forbidden: true
}
},
bridge: {
pilot: {
pilot_scope: "counterparty_movement_evidence_query_movements_v1"
}
}
},
dialog_continuation_contract_v2: {
decision: "continue_previous"
}
}
} as any;
const afterMovements = evolveAddressNavigationStateWithAssistantItem(afterDocs, movementsItem, 7);
expect(afterMovements.result_sets.at(-1)?.intent).toBe("bank_operations_by_counterparty");
expect(afterMovements.result_sets.at(-1)?.type).toBe("bank_operations_list");
expect(afterMovements.session_context.active_focus_object?.label).toBe("Группа СВК");
expect(afterMovements.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
valueFlowBundle
);
});
it("keeps selected counterparty focus for company boundary summaries", () => {
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
const assistantItem = {

View File

@ -684,6 +684,27 @@ describe("assistantContinuityPolicy organization authority", () => {
});
});
it("carries value-flow temporal scope into follow-up evidence drilldowns", () => {
const filters = applyTemporalCarryoverFilters(
{
counterparty: "Группа СВК"
},
{
as_of_date: null,
period_from: "2020-01-01",
period_to: "2020-12-31"
},
null,
"customer_revenue_and_payments"
);
expect(filters).toEqual({
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("resolves inventory root pivots through one shared helper", () => {
const flags = resolveInventoryFollowupPivotFlags(
{ intent: "inventory_on_hand_as_of_date" },

View File

@ -130,15 +130,14 @@ describe("assistant MCP discovery answer adapter", () => {
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.unknown_lines].join("\n");
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines].join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(answerText).toContain("0 руб.");
expect(answerText).toContain("исходящих платежей/списаний не найдено");
expect(answerText).toContain("not_found");
expect(answerText).toContain("Группа СВК");
expect(answerText).toContain("2020");
expect(draft.next_step_line).toContain("соседних периодах");
expect(draft.next_step_line).toContain("двусторонний денежный поток");
expect(draft.unknown_lines.join("\n")).toContain("outside the checked period");
});
it("turns generic document evidence into a bounded document answer draft", async () => {
@ -1464,7 +1463,7 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
});
it("renders zero-row supplier payout as a checked negative with period and counterparty", async () => {
it("renders zero-row supplier payout as a confirmed scoped zero with period and counterparty", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
@ -1479,18 +1478,18 @@ describe("assistant MCP discovery answer adapter", () => {
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const confirmedText = draft.confirmed_lines.join("\n");
const inferenceText = draft.inference_lines.join("\n");
const unknownText = draft.unknown_lines.join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("Группа СВК");
expect(draft.headline).toContain("2020");
expect(draft.headline).toContain("исходящих платежей");
expect(draft.headline).toContain("0 руб.");
expect(confirmedText).toContain("Группа СВК");
expect(confirmedText).toContain("ООО Альтернатива Плюс");
expect(confirmedText).toContain("2020");
expect(confirmedText).toContain("не найдено");
expect(unknownText).toContain("вне периода 2020");
expect(unknownText).toContain("вне доступного банковского контура");
expect(confirmedText).toContain("0 руб.");
expect(confirmedText).toContain("Граница");
expect(inferenceText).toContain("not_found result was treated as zero");
expect(unknownText).toContain("outside the checked period");
});
it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => {

View File

@ -1650,6 +1650,38 @@ describe("assistant MCP discovery pilot executor", () => {
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
});
it("treats zero-row supplier payout query as confirmed not-found inside checked scope", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const deps = buildDeps([]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.evidence.answer_permission).toBe("confirmed_answer");
expect(result.evidence.confirmed_facts[0]).toContain("not_found");
expect(result.reason_codes).toContain("pilot_derived_value_flow_from_confirmed_rows");
expect(result.evidence.reason_codes).toContain("confirmed_no_match_fact_with_allowed_mcp_evidence");
expect(result.derived_value_flow).toMatchObject({
value_flow_direction: "outgoing_supplier_payout",
counterparty: "SVK",
period_scope: "2020",
rows_matched: 0,
rows_with_amount: 0,
total_amount: 0,
total_amount_human_ru: "0 руб."
});
});
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -1602,8 +1602,9 @@ describe("assistant MCP discovery response candidate", () => {
expect(firstLine).toContain("По подтвержденным строкам 1С");
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
expect(firstLine).toContain("исходящие 35 439 044,74 руб.");
expect(firstLine).toContain("не аудиторское заключение");
expect(firstLine).toContain("не подтвержденная чистая прибыль");
expect(firstLine).toContain("не финансовый аудит");
expect(firstLine).toContain("не подтверждение чистой прибыли");
expect(firstLine).not.toContain("аудиторское заключение");
expect(candidate.reply_text).toContain("Интерпретация:");
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");

View File

@ -757,6 +757,72 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
});
it("overrides exact document-list replies when they contradict the discovery period scope", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Контрагент: Группа СВК. Найдено документов: 19.\n" +
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07 | аналитика: Группа СВК",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "list_documents_by_counterparty",
selected_recipe: "address_documents_by_counterparty_v1",
mcp_call_status: "matched_non_empty",
response_type: "FACTUAL_LIST",
truth_mode: "confirmed",
extracted_filters: {
counterparty: "Группа СВК",
as_of_date: "2026-06-15"
},
dialog_continuation_contract_v2: {
target_intent: "list_documents_by_counterparty"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
},
data_need_graph: {
business_fact_family: "document_evidence",
action_family: "list_documents",
subject_candidates: ["Группа СВК"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "bounded_inference_only",
headline: "По документам по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
confirmed_lines: [],
inference_lines: ["Подтвержденных строк документов за 2020 этим поиском не найдено."],
unknown_lines: ["Полный исторический срез вне 2020 этим поиском не подтвержден."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("за 2020");
expect(result.reply_text).not.toContain("2021-11-10");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
});
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",