Compare commits

..

No commits in common. "e9f0ef6b4129e1e30d010c8ef9272afd742629fe" and "9e2bc8cb6cf52cf2bba6905358db7b5037f392cc" have entirely different histories.

54 changed files with 2052 additions and 3881 deletions

File diff suppressed because it is too large Load Diff

View File

@ -66,9 +66,6 @@
"required_patterns_any": [ "required_patterns_any": [
"(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)" "(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)"
], ],
"artifact_path_include_patterns": [
"(?i)steps[\\\\/][^\\\\/]*(?:margin|profitability|марж)"
],
"artifact_path_exclude_patterns": [ "artifact_path_exclude_patterns": [
"(?i)selected_item", "(?i)selected_item",
"(?i)supplier", "(?i)supplier",
@ -211,45 +208,6 @@
"forbidden_patterns": ["(?im)^\\s*(Что проверили|Опорные документы):"] "forbidden_patterns": ["(?im)^\\s*(Что проверили|Опорные документы):"]
} }
}, },
"counterparty_value_flow_required_surface": {
"kind": "answer_text_required_any",
"automation_level": "automatic",
"description": "Counterparty value-flow answers must surface counterparty scope, incoming amount, outgoing amount, and net/saldo wording.",
"issue_codes": ["counterparty_value_flow_misrouted_to_company_profit"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"artifact_path_include_patterns": [
"(?i)(s01_svk_money_documents|s01_select_svk_counterparty_money|step_05_net_after_payout)"
],
"required_patterns_any": [
"(?is)(?=.*(СВК|Группа\\s+СВК))(?=.*(входящ|получил|получено|получили))(?=.*(исходящ|заплатил|заплачено|заплатили|ушло))(?=.*(нетто|сальдо|разниц|чистый\\s+денежный))"
]
}
},
"counterparty_value_flow_profit_accounts_forbidden": {
"kind": "answer_text_regex_forbidden",
"automation_level": "automatic",
"description": "Counterparty received/paid/net-flow answers must not use company profit or 90/91/99 accounts as the direct answer.",
"issue_codes": ["counterparty_value_flow_misrouted_to_company_profit"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"artifact_path_include_patterns": [
"(?i)(s01_svk_money_documents|s01_select_svk_counterparty_money|step_03_incoming_by_resolved_entity|step_04_payout_switch_by_resolved_entity|step_05_net_after_payout)"
],
"forbidden_patterns": ["(?i)(сч[её]т\\s*(90|91|99)|90[\\./](01|02|09)|91[\\./]|99\\b|company[- ]level\\s+profit)"]
}
},
"bank_counterparty_boundary_required": {
"kind": "answer_text_required_any",
"automation_level": "semi_automatic",
"description": "Money-leader answers that explicitly mention bank role must include a boundary instead of presenting a bank as an ordinary customer/supplier.",
"issue_codes": ["bank_counterparty_misclassified_as_business_partner"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"artifact_path_include_patterns": ["(?i)(s02_colloquial_money_leaders|step_03_sberbank_role_after_role_tail_overview)"],
"required_patterns_any": ["(?is)(банк|банковск|финансов|не\\s+обычн|роль|назначени[ея]|плат[её]ж)"]
}
},
"route_candidate_needs_enablement": { "route_candidate_needs_enablement": {
"kind": "stage_review_signal", "kind": "stage_review_signal",
"automation_level": "semi_automatic", "automation_level": "semi_automatic",

View File

@ -411,11 +411,7 @@
"severity": "P0", "severity": "P0",
"business_meaning": "Контрагентский вопрос про получили/заплатили/нетто отвечен company-level прибылью или 90/91/99 вместо контрагентского денежного потока.", "business_meaning": "Контрагентский вопрос про получили/заплатили/нетто отвечен company-level прибылью или 90/91/99 вместо контрагентского денежного потока.",
"root_layers": ["followup_action_resolution_gap", "answer_shape_mismatch", "domain_scope_leak"], "root_layers": ["followup_action_resolution_gap", "answer_shape_mismatch", "domain_scope_leak"],
"detectors": [ "detectors": ["counterparty_value_flow_profit_mismatch"],
"counterparty_value_flow_required_surface",
"counterparty_value_flow_profit_accounts_forbidden",
"counterparty_value_flow_profit_mismatch"
],
"allowed_patch_targets": [ "allowed_patch_targets": [
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts", "llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
"llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts", "llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts",
@ -493,10 +489,7 @@
"severity": "P1", "severity": "P1",
"business_meaning": "Банк/финансовая организация ошибочно подана как обычный клиент/поставщик без роли, назначения платежа и договорного контекста.", "business_meaning": "Банк/финансовая организация ошибочно подана как обычный клиент/поставщик без роли, назначения платежа и договорного контекста.",
"root_layers": ["business_semantic_role_gap", "domain_scope"], "root_layers": ["business_semantic_role_gap", "domain_scope"],
"detectors": [ "detectors": ["bank_counterparty_role_boundary_missing"],
"bank_counterparty_boundary_required",
"bank_counterparty_role_boundary_missing"
],
"allowed_patch_targets": [ "allowed_patch_targets": [
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts", "llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts", "llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",

View File

@ -1694,8 +1694,7 @@ function hasNomenclatureMarginRankingSignal(text) {
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized); const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized); const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized); const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized); return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasVatPeriodInspectionBridgeSignal(text) { function hasVatPeriodInspectionBridgeSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase(); const normalized = String(text ?? "").trim().toLowerCase();

View File

@ -33,8 +33,7 @@ function hasInventoryMarginRankingSignal(text) {
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized); const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized); const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized); const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized); return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasInventoryOnHandSignal(text) { function hasInventoryOnHandSignal(text) {
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text); const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);

View File

@ -5,7 +5,6 @@ exports.cloneAddressNavigationState = cloneAddressNavigationState;
exports.normalizeAddressNavigationState = normalizeAddressNavigationState; exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem; exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
const nanoid_1 = require("nanoid"); const nanoid_1 = require("nanoid");
const addressTextRepair_1 = require("./addressTextRepair");
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy"); const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
const addressNavigation_1 = require("../types/addressNavigation"); const addressNavigation_1 = require("../types/addressNavigation");
const MAX_RESULT_SETS = 40; const MAX_RESULT_SETS = 40;
@ -127,48 +126,6 @@ function readNavigationDiscoveryCounterparty(debug) {
} }
return null; 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) { function toAddressFocusObjectType(value) {
const normalized = toNonEmptyString(value); const normalized = toNonEmptyString(value);
if (!normalized) { if (!normalized) {
@ -338,32 +295,6 @@ function normalizeFilters(value) {
} }
return { ...record }; return { ...record };
} }
function isBusinessOverviewRoleCounterpartyFragment(value) {
const text = (0, addressTextRepair_1.normalizeRussianComparableText)(value);
if (!text) {
return false;
}
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
if (!hasRoleWord) {
return false;
}
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
}
function shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty) {
return (toNonEmptyString(debug.mcp_discovery_selected_chain_id) === "business_overview" &&
debug.mcp_discovery_response_applied === true &&
isBusinessOverviewRoleCounterpartyFragment(counterparty));
}
function sanitizeBusinessOverviewNavigationFilters(filters, debug) {
if (!shouldSuppressBusinessOverviewCounterpartyFocus(debug, filters.counterparty)) {
return filters;
}
const { counterparty: _counterparty, ...rest } = filters;
return rest;
}
function resolveNavigationAction(debug, hasFocusObject) { function resolveNavigationAction(debug, hasFocusObject) {
const continuationContract = toObject(debug.dialog_continuation_contract_v2); const continuationContract = toObject(debug.dialog_continuation_contract_v2);
const decision = toNonEmptyString(continuationContract?.decision); const decision = toNonEmptyString(continuationContract?.decision);
@ -406,7 +337,8 @@ function readBusinessOverviewComparisonProofBundles(debug) {
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1); const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input); const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref); const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const pilot = readDiscoveryPilot(debug); const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ?? const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow); cloneRecord(pilot?.derived_bidirectional_value_flow);
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle); const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
@ -424,24 +356,12 @@ function readBusinessOverviewComparisonProofBundles(debug) {
counterparty_document_bundle: counterpartyDocumentBundle 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) { function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
const extractedFilters = toObject(debug.extracted_filters) ?? {}; const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id); const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) { if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug); const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty)) { if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt); return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
} }
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString); const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
@ -449,8 +369,8 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
return buildFocusObject("organization", organization, resultSetId, createdAt); return buildFocusObject("organization", organization, resultSetId, createdAt);
} }
} }
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) { if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty = readCurrentDiscoveryCounterparty(debug); const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) { if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt); return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
} }
@ -640,23 +560,24 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
} }
const debug = item.debug; const debug = item.debug;
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id); 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 detectedIntent = toNonEmptyString(debug.detected_intent);
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain); const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(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) { if (intent === "unknown" && !trackableDiscoveryTurn) {
return state; return state;
} }
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString(); const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
const resultSetId = `rs-${item.message_id}`; const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain; const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug); const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString); const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
const derivedCounterpartyScope = isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true const derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readCurrentDiscoveryCounterparty(debug) ? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
: null; : null;
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization) const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
? { ? {
@ -689,23 +610,14 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt); const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt); const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject); const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
const rawComparisonCounterparty = debug.mcp_discovery_response_applied === true const comparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? selectedDiscoveryChain === "business_overview"
? readNavigationDiscoveryCounterparty(debug) ? readNavigationDiscoveryCounterparty(debug)
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
? readCurrentDiscoveryCounterparty(debug)
: null
: null;
const comparisonCounterparty = rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
? rawComparisonCounterparty
: null; : null;
const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization) ? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
: null; : null;
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug) ? readBusinessOverviewComparisonProofBundles(debug)
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readCounterpartyValueFlowProofBundles(debug)
: null; : null;
const inheritedComparisonScope = state.session_context.comparison_scope; const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles = comparisonCounterparty && const inheritedComparisonProofBundles = comparisonCounterparty &&

View File

@ -1313,15 +1313,6 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
intent === "vat_payable_confirmed_as_of_date"; intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged); const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous); 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 currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
const currentContractExplicit = toNonEmptyString(merged.contract); const currentContractExplicit = toNonEmptyString(merged.contract);
const currentItemExplicit = toNonEmptyString(merged.item); const currentItemExplicit = toNonEmptyString(merged.item);
@ -1396,7 +1387,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
} }
reasons.push("period_from_followup_context"); reasons.push("period_from_followup_context");
} }
if ((!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) && if (!currentHasPeriod &&
previousHasPeriod && previousHasPeriod &&
hasFollowupSignal && hasFollowupSignal &&
!hasExplicitPeriodInMessage && !hasExplicitPeriodInMessage &&

View File

@ -411,25 +411,11 @@ function isReferentialCounterpartyPlaceholder(value) {
} }
function normalizeCounterpartyCandidate(value, toNonEmptyString) { function normalizeCounterpartyCandidate(value, toNonEmptyString) {
const text = toNonEmptyString(value); const text = toNonEmptyString(value);
if (!text || isReferentialCounterpartyPlaceholder(text) || isCounterpartyRoleFragment(text)) { if (!text || isReferentialCounterpartyPlaceholder(text)) {
return null; return null;
} }
return text; return text;
} }
function isCounterpartyRoleFragment(value) {
const text = (0, addressTextRepair_1.normalizeRussianComparableText)(value);
if (!text) {
return false;
}
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
if (!hasRoleWord) {
return false;
}
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
}
function sameCounterpartyCandidate(left, right) { function sameCounterpartyCandidate(left, right) {
return Boolean(left && return Boolean(left &&
right && right &&
@ -604,9 +590,6 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT
const organization = readAddressDebugOrganization(debug, toNonEmptyString); const organization = readAddressDebugOrganization(debug, toNonEmptyString);
const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty); const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString); const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
if (!existingCounterparty) {
delete nextFilters.counterparty;
}
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) { if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
nextFilters.counterparty = counterparty; nextFilters.counterparty = counterparty;
} }
@ -778,11 +761,7 @@ function resolveFollowupTargetIntent(inventoryPurchaseDateVatBridge, selectedObj
} }
function shouldUseNavigationTemporalCarryover(sourceIntentHint) { function shouldUseNavigationTemporalCarryover(sourceIntentHint) {
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint); const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
return (normalizedIntent === "customer_revenue_and_payments" || return (normalizedIntent === "inventory_on_hand_as_of_date" ||
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_supplier_stock_overlap_as_of_date" ||
normalizedIntent === "inventory_purchase_provenance_for_item" || normalizedIntent === "inventory_purchase_provenance_for_item" ||
normalizedIntent === "inventory_purchase_documents_for_item" || normalizedIntent === "inventory_purchase_documents_for_item" ||

View File

@ -212,7 +212,7 @@ function valueFlowDirectionLabelRu(pilot) {
? "исходящих платежей/списаний" ? "исходящих платежей/списаний"
: "входящих денежных поступлений"; : "входящих денежных поступлений";
} }
function valueFlowZeroResultInsufficiencyLine(pilot) { function valueFlowZeroResultConfirmedLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) { if (!hasExecutedZeroValueFlowRows(pilot)) {
return null; return null;
} }
@ -224,22 +224,7 @@ function valueFlowZeroResultInsufficiencyLine(pilot) {
const period = explicitDateScope(pilot); const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : ""; const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне"; const periodPart = period ? ` за период ${period}` : " в проверенном окне";
const direction = valueFlowDirectionLabelRu(pilot); return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${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) { function valueFlowZeroResultUnknownLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) { if (!hasExecutedZeroValueFlowRows(pilot)) {
@ -254,11 +239,11 @@ function valueFlowZeroResultUnknownLine(pilot) {
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`; return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
} }
function valueFlowZeroResultHeadline(pilot) { function valueFlowZeroResultHeadline(pilot) {
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot); const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
if (!insufficiencyLine) { if (!confirmedLine) {
return null; return null;
} }
return insufficiencyLine; return confirmedLine;
} }
function hasAllTimeScope(pilot) { function hasAllTimeScope(pilot) {
return (dryRunHasAxis(pilot, "all_time_scope") || return (dryRunHasAxis(pilot, "all_time_scope") ||
@ -953,12 +938,6 @@ function nextStepFor(mode, pilot) {
} }
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном."; return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
} }
if (mode === "checked_sources_only" && hasExecutedZeroValueFlowRows(pilot)) {
const flowLabel = pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
? "исходящие платежи/списания"
: "входящие денежные поступления";
return `Можно следующим шагом проверить ${flowLabel} по этому контрагенту в соседних периодах, по договорам или документам, либо собрать двусторонний денежный поток, чтобы отделить отсутствие строк в текущем срезе от операций вне проверенного окна.`;
}
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) { if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
return pilot.derived_business_overview return pilot.derived_business_overview
? businessOverviewNextStepLine(pilot.derived_business_overview) ? businessOverviewNextStepLine(pilot.derived_business_overview)
@ -1861,6 +1840,8 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
? [derivedValueLine] ? [derivedValueLine]
: derivedValueLine : derivedValueLine
? [derivedValueLine, ...monthlyConfirmedLines] ? [derivedValueLine, ...monthlyConfirmedLines]
: valueFlowZeroResultConfirmedLine(pilot)
? [valueFlowZeroResultConfirmedLine(pilot)]
: derivedEntityResolutionLine : derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine] ? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine : derivedMetadataLine
@ -1872,9 +1853,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
? pilot.derived_metadata_surface.available_fields.length > 0 ? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts) ? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."] : ["Детальный список полей этих объектов этим шагом не получен."]
: appendValueFlowZeroResultUnknown(valueFlowZeroResultCheckedLine(pilot) : appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
? [valueFlowZeroResultCheckedLine(pilot), ...rankedValueFlowUnknownLines(pilot)]
: rankedValueFlowUnknownLines(pilot), pilot);
return { return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter", policy_owner: "assistantMcpDiscoveryAnswerAdapter",

View File

@ -2,7 +2,6 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0; exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph; exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
const addressTextRepair_1 = require("./addressTextRepair");
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1"; exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
function toNonEmptyString(value) { function toNonEmptyString(value) {
if (value === null || value === undefined) { if (value === null || value === undefined) {
@ -204,28 +203,6 @@ function hasDetailedBusinessOverviewMoneyBreakdownHint(rawUtterance) {
} }
return /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*|\u043f\u043e\u0434\u0440\u043e\u0431\p{L}*|\u0440\u0430\u0437\u0431\u0435\p{L}*|\u0440\u0430\u0437\u0432\u0435\u0440\u043d\p{L}*|\u043a\u0442\u043e\s+\u0433\u043b\u0430\u0432\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043a\u043b\u0438\u0435\u043d\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043f\u043e\u0441\u0442\u0430\u0432\p{L}*|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043f\u043e\u0442\u043e\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\p{L}*|detail|breakdown|drill\s*down|main\s+customer|main\s+supplier)/iu.test(rawUtterance); return /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*|\u043f\u043e\u0434\u0440\u043e\u0431\p{L}*|\u0440\u0430\u0437\u0431\u0435\p{L}*|\u0440\u0430\u0437\u0432\u0435\u0440\u043d\p{L}*|\u043a\u0442\u043e\s+\u0433\u043b\u0430\u0432\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043a\u043b\u0438\u0435\u043d\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043f\u043e\u0441\u0442\u0430\u0432\p{L}*|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043f\u043e\u0442\u043e\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\p{L}*|detail|breakdown|drill\s*down|main\s+customer|main\s+supplier)/iu.test(rawUtterance);
} }
function hasWideBusinessOverviewSurfaceHint(rawUtterance) {
if (!rawUtterance) {
return false;
}
const textVariants = Array.from(new Set([rawUtterance, (0, addressTextRepair_1.normalizeRussianComparableText)(rawUtterance)]));
if (textVariants.some((text) => /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0434\u0435\u043d\p{L}*)/iu.test(text))) {
return false;
}
const hasBroadCue = textVariants.some((text) => /(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]*\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\p{L}*\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0446\u0435\u043d\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|business\s+overview|business\s+review|company\s+overview)/iu.test(text));
if (!hasBroadCue) {
return false;
}
const surfaceMarkers = [
/(?:\u043d\u0434\u0441|vat|\u043d\u0430\u043b\u043e\u0433\p{L}*)/iu,
/(?:\u0434\u043e\u043b\u0433\p{L}*|\u0437\u0430\u0434\u043e\u043b\u0436\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442\u043a|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440\p{L}*)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b)/iu,
/(?:\u0447\u0442\u043e\s+(?:\u043f\u043e\u043a\u0430\s+)?\u043d\u0435\u043b\u044c\u0437\u044f\s+\u0443\u0442\u0432\u0435\u0440\u0436\u0434|\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d|\u0433\u0440\u0430\u043d\u0438\u0446\p{L}*)/iu
];
return textVariants.some((text) => surfaceMarkers.filter((marker) => marker.test(text)).length >= 3);
}
function supportsOrganizationScopedOpenTotal(action) { function supportsOrganizationScopedOpenTotal(action) {
return action === "turnover" || action === "payout"; return action === "turnover" || action === "payout";
} }
@ -444,8 +421,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const rawAction = lower(turnMeaning?.asked_action_family); const rawAction = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family); const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance); const rawUtterance = lower(input.rawUtterance);
const rawQuestionSource = [input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "); const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
const rawQuestionSignal = lower(rawQuestionSource);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis); const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need); const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
@ -463,13 +439,10 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
}); });
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal); const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal); const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
const wideBusinessOverviewSurfaceHint = hasWideBusinessOverviewSurfaceHint(rawQuestionSource);
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) && Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint; !detailedBusinessOverviewMoneyBreakdownHint;
const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal) &&
!wideBusinessOverviewSurfaceHint;
const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" && const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" &&
Boolean(oneSidedValueAction) && Boolean(oneSidedValueAction) &&
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value"); (rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
@ -601,9 +574,6 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (businessFactFamily === "business_overview" && !explicitDateScope) { if (businessFactFamily === "business_overview" && !explicitDateScope) {
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope"); pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
} }
if (initialBusinessFactFamily === "business_overview" && wideBusinessOverviewSurfaceHint) {
pushReason(reasonCodes, "data_need_graph_business_overview_wide_surface_preserved");
}
if (directBusinessOverviewMoneyAnswerHint) { if (directBusinessOverviewMoneyAnswerHint) {
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer"); pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
} }

View File

@ -2209,28 +2209,9 @@ function deriveBusinessOverviewContractUsageProfile(result, periodScope) {
}; };
} }
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) { function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
if (!result || result.error) { if (!result || result.error || result.matched_rows <= 0) {
return null; 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 totalAmount = 0;
let rowsWithAmount = 0; let rowsWithAmount = 0;
for (const row of result.rows) { for (const row of result.rows) {
@ -3945,30 +3926,16 @@ function buildMovementConfirmedFacts(result, counterparty, periodScope) {
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`]; return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
} }
function buildValueFlowConfirmedFacts(result, counterparty, direction) { function buildValueFlowConfirmedFacts(result, counterparty, direction) {
if (result.error) { if (result.error || result.matched_rows <= 0) {
return []; return [];
} }
if (direction === "outgoing_supplier_payout") { 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 [ return [
counterparty counterparty
? `1C supplier-payout rows were found for counterparty ${counterparty}` ? `1C supplier-payout rows were found for counterparty ${counterparty}`
: "1C supplier-payout rows were found for the requested counterparty scope" : "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 [ return [
counterparty counterparty
? `1C value-flow rows were found for counterparty ${counterparty}` ? `1C value-flow rows were found for counterparty ${counterparty}`
@ -4148,12 +4115,6 @@ function buildValueFlowInferredFacts(derived) {
return []; return [];
} }
const facts = []; 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") { if (derived.value_flow_direction === "outgoing_supplier_payout") {
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"); facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
} }

View File

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

View File

@ -211,15 +211,9 @@ function localizeLine(value) {
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) { if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе."; 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)) { if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе."; 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); 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) { if (openScopeBidirectionalMatch) {
const incoming = openScopeBidirectionalMatch[1] === "found" const incoming = openScopeBidirectionalMatch[1] === "found"
@ -244,10 +238,6 @@ function localizeLine(value) {
if (valueFlowMatch) { if (valueFlowMatch) {
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`; 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)) { if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру."; return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
} }
@ -269,10 +259,6 @@ function localizeLine(value) {
if (supplierPayoutMatch) { if (supplierPayoutMatch) {
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`; 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)) { if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру."; return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
} }
@ -306,12 +292,6 @@ function localizeLine(value) {
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) { if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С."; 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)) { if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С."; return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
} }
@ -723,7 +703,6 @@ function businessOverviewOrganizationScopeLabel(value) {
} }
const comparable = compactComparable(text); const comparable = compactComparable(text);
if (/^(?:с|без)\s+разбивк/.test(comparable) || if (/^(?:с|без)\s+разбивк/.test(comparable) ||
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(comparable) ||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) { /\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
return null; return null;
} }
@ -1179,7 +1158,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
outgoingAmount ? `исходящие ${outgoingAmount}` : null, outgoingAmount ? `исходящие ${outgoingAmount}` : null,
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
].filter((value) => Boolean(value)); ].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("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
lines.push("Что видно в ограниченном денежном срезе:"); lines.push("Что видно в ограниченном денежном срезе:");
if (incomingAmount) { if (incomingAmount) {
@ -1214,7 +1193,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push(`- ${localizeLine(debtLine)}`); lines.push(`- ${localizeLine(debtLine)}`);
} }
if (!limitLine) { if (!limitLine) {
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли."); lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
} }
const missingOverviewFamilies = []; const missingOverviewFamilies = [];
if (!taxLine) { if (!taxLine) {

View File

@ -364,51 +364,6 @@ function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarificati
const boundedMovementAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true; const boundedMovementAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed; return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
} }
function hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "clarification_candidate" && candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const separateCounterpartyCandidates = readStringArray(turnMeaning?.business_overview_separate_entity_candidates);
const hasPreviousCounterpartyBundle = Boolean(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle) ||
toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
const asksForBoundarySummary = Boolean((selectedChainId === "business_overview" ||
discoveryFamily === "business_overview" ||
askedDomain === "business_overview") &&
(separateCounterpartyCandidates.length > 0 || hasPreviousCounterpartyBundle));
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_")));
return asksForBoundarySummary && staleInventoryItemClarification;
}
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) { function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) { if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false; return false;
@ -615,53 +570,6 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
routeMode === "exact" || routeMode === "exact" ||
hasFullConfirmedTruth(input)); 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) { function hasExactDocumentListAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
@ -1015,8 +923,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint); const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint); const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint); const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint); const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint); const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint); const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint); const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
@ -1027,20 +934,16 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate); const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate); const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
const groundedMovementCandidatePriority = hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate); const groundedMovementCandidatePriority = hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
const businessOverviewBoundaryCandidatePriority = hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
const groundedValueFlowRankingCandidatePriority = hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate); const groundedValueFlowRankingCandidatePriority = hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply && const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply &&
!discoveryClarificationCandidatePriority && !discoveryClarificationCandidatePriority &&
!groundedValueFlowCandidatePriority && !groundedValueFlowCandidatePriority &&
!groundedMovementCandidatePriority && !groundedMovementCandidatePriority &&
!businessOverviewBoundaryCandidatePriority &&
!groundedValueFlowRankingCandidatePriority; !groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery; const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
!groundedValueFlowRankingCandidatePriority && const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
!documentListTemporalConflictWithDiscovery; const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply && const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning && !semanticConflictWithDiscoveryTurnMeaning &&
!valueFlowActionConflictWithDiscoveryTurnMeaning; !valueFlowActionConflictWithDiscoveryTurnMeaning;
@ -1074,9 +977,6 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (evidenceLaneConflictWithDiscoveryTurnMeaning) { if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override"); 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) { if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply"); pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
} }
@ -1089,9 +989,6 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (groundedMovementCandidatePriority) { if (groundedMovementCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification"); pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification");
} }
if (businessOverviewBoundaryCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification");
}
if (groundedValueFlowRankingCandidatePriority) { if (groundedValueFlowRankingCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply"); pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply");
} }

View File

@ -79,10 +79,6 @@ function isReferentialOrganizationPlaceholder(value) {
if (isNegativeCounterpartyScopePlaceholder(value)) { if (isNegativeCounterpartyScopePlaceholder(value)) {
return true; return true;
} }
const text = compactLower((0, addressTextRepair_1.repairAddressMojibakeText)(value));
if (/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(text)) {
return true;
}
return new Set([ return new Set([
"эта организация", "эта организация",
"этой организации", "этой организации",
@ -886,10 +882,6 @@ function hasBidirectionalValueFlowSignal(text) {
if (/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text)) { if (/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text)) {
return true; return true;
} }
if (/(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(text) &&
hasPayoutSignal(text)) {
return true;
}
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text); return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
} }
function hasValueRankingSignal(text) { function hasValueRankingSignal(text) {
@ -929,7 +921,7 @@ function normalizeLooseOrganizationAlias(value) {
if (!text) { if (!text) {
return null; return null;
} }
if (isReferentialOrganizationPlaceholder(text)) { if (isNegativeCounterpartyScopePlaceholder(text)) {
return null; return null;
} }
const normalized = text const normalized = text

View File

@ -184,10 +184,7 @@ function detectScopedCounterpartyEntity(text) {
if (!rawEntity) { if (!rawEntity) {
continue; continue;
} }
const entity = rawEntity const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
.replace(/^["'«»]+|["'«»]+$/gu, "")
.replace(/^(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043b\u0438\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?)\s+/iu, "")
.trim();
if (entity.length >= 2 && !ignored.has(entity)) { if (entity.length >= 2 && !ignored.has(entity)) {
return entity; return entity;
} }
@ -198,8 +195,7 @@ function detectCounterpartyBidirectionalValueFlowFamily(text) {
const hasNetCue = /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text); const hasNetCue = /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
const hasIncomingCue = /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text); const hasIncomingCue = /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
const hasOutgoingCue = /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text); const hasOutgoingCue = /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
const hasOverallMoneyMeaningCue = /(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(text); if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue) || (hasOverallMoneyMeaningCue && hasOutgoingCue))) {
return null; return null;
} }
const entity = detectScopedCounterpartyEntity(text); const entity = detectScopedCounterpartyEntity(text);
@ -413,8 +409,6 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
? null ? null
: detectBroadBusinessEvaluation(joinedText); : detectBroadBusinessEvaluation(joinedText);
const explicitIntentCandidate = broadBusinessEvaluation?.family const explicitIntentCandidate = broadBusinessEvaluation?.family
? null
: counterpartyBidirectionalValueFlow?.family
? null ? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null); : supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family const unsupportedFamily = broadBusinessEvaluation?.family

View File

@ -2200,11 +2200,7 @@ function hasNomenclatureMarginRankingSignal(text: string): boolean {
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test( /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
normalized normalized
); );
const hasInspectionCue = return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
normalized
);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasVatPeriodInspectionBridgeSignal(text: string): boolean { function hasVatPeriodInspectionBridgeSignal(text: string): boolean {

View File

@ -57,11 +57,7 @@ function hasInventoryMarginRankingSignal(text: string): boolean {
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test( /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
normalized normalized
); );
const hasInspectionCue = return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
normalized
);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasInventoryOnHandSignal(text: string): boolean { function hasInventoryOnHandSignal(text: string): boolean {

View File

@ -1,7 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import type { AssistantConversationItem } from "../types/assistant"; import type { AssistantConversationItem } from "../types/assistant";
import type { AddressIntent } from "../types/addressQuery"; import type { AddressIntent } from "../types/addressQuery";
import { normalizeRussianComparableText } from "./addressTextRepair";
import { import {
readAddressDebugCounterparty, readAddressDebugCounterparty,
readAddressDebugItem, readAddressDebugItem,
@ -151,56 +150,6 @@ function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): st
return null; 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 { function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
const normalized = toNonEmptyString(value); const normalized = toNonEmptyString(value);
if (!normalized) { if (!normalized) {
@ -397,43 +346,6 @@ function normalizeFilters(value: unknown): Record<string, unknown> {
return { ...record }; return { ...record };
} }
function isBusinessOverviewRoleCounterpartyFragment(value: unknown): boolean {
const text = normalizeRussianComparableText(value);
if (!text) {
return false;
}
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
if (!hasRoleWord) {
return false;
}
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
}
function shouldSuppressBusinessOverviewCounterpartyFocus(
debug: Record<string, unknown>,
counterparty: unknown
): boolean {
return (
toNonEmptyString(debug.mcp_discovery_selected_chain_id) === "business_overview" &&
debug.mcp_discovery_response_applied === true &&
isBusinessOverviewRoleCounterpartyFragment(counterparty)
);
}
function sanitizeBusinessOverviewNavigationFilters(
filters: Record<string, unknown>,
debug: Record<string, unknown>
): Record<string, unknown> {
if (!shouldSuppressBusinessOverviewCounterpartyFocus(debug, filters.counterparty)) {
return filters;
}
const { counterparty: _counterparty, ...rest } = filters;
return rest;
}
function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject: boolean): AddressNavigationAction { function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject: boolean): AddressNavigationAction {
const continuationContract = toObject(debug.dialog_continuation_contract_v2); const continuationContract = toObject(debug.dialog_continuation_contract_v2);
const decision = toNonEmptyString(continuationContract?.decision); const decision = toNonEmptyString(continuationContract?.decision);
@ -487,7 +399,8 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1); const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toObject(entryPoint?.turn_input); const turnInput = toObject(entryPoint?.turn_input);
const turnMeaning = toObject(turnInput?.turn_meaning_ref); const turnMeaning = toObject(turnInput?.turn_meaning_ref);
const pilot = readDiscoveryPilot(debug); const bridge = toObject(entryPoint?.bridge);
const pilot = toObject(bridge?.pilot);
const counterpartyValueFlowBundle = const counterpartyValueFlowBundle =
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ?? cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
cloneRecord(pilot?.derived_bidirectional_value_flow); cloneRecord(pilot?.derived_bidirectional_value_flow);
@ -508,25 +421,12 @@ 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 { function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
const extractedFilters = toObject(debug.extracted_filters) ?? {}; const extractedFilters = toObject(debug.extracted_filters) ?? {};
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id); const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) { if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug); const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty)) { if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt); return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
} }
const organization = readAddressDebugOrganization(debug, toNonEmptyString); const organization = readAddressDebugOrganization(debug, toNonEmptyString);
@ -534,8 +434,9 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
return buildFocusObject("organization", organization, resultSetId, createdAt); return buildFocusObject("organization", organization, resultSetId, createdAt);
} }
} }
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) { if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
const counterparty = readCurrentDiscoveryCounterparty(debug); const counterparty =
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
if (counterparty) { if (counterparty) {
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt); return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
} }
@ -758,28 +659,27 @@ export function evolveAddressNavigationStateWithAssistantItem(
} }
const debug = item.debug as unknown as Record<string, unknown>; const debug = item.debug as unknown as Record<string, unknown>;
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id); 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 detectedIntent = toNonEmptyString(debug.detected_intent);
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain); const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(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) { if (intent === "unknown" && !trackableDiscoveryTurn) {
return state; return state;
} }
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString(); const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
const resultSetId = `rs-${item.message_id}`; const resultSetId = `rs-${item.message_id}`;
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain; const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug); const filters = normalizeFilters(debug.extracted_filters);
const derivedOrganizationScope = const derivedOrganizationScope =
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString); resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
const derivedCounterpartyScope = const derivedCounterpartyScope =
isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readCurrentDiscoveryCounterparty(debug) ? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
: null; : null;
const filtersWithDerivedScope = const filtersWithDerivedScope =
derivedOrganizationScope && !toNonEmptyString(filters.organization) derivedOrganizationScope && !toNonEmptyString(filters.organization)
@ -819,17 +719,9 @@ export function evolveAddressNavigationStateWithAssistantItem(
debugFocusObject, debugFocusObject,
primaryEntityFocusObject primaryEntityFocusObject
); );
const rawComparisonCounterparty =
debug.mcp_discovery_response_applied === true
? selectedDiscoveryChain === "business_overview"
? readNavigationDiscoveryCounterparty(debug)
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
? readCurrentDiscoveryCounterparty(debug)
: null
: null;
const comparisonCounterparty = const comparisonCounterparty =
rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty) selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? rawComparisonCounterparty ? readNavigationDiscoveryCounterparty(debug)
: null; : null;
const comparisonOrganization = const comparisonOrganization =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
@ -838,8 +730,6 @@ export function evolveAddressNavigationStateWithAssistantItem(
const currentComparisonProofBundles = const currentComparisonProofBundles =
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
? readBusinessOverviewComparisonProofBundles(debug) ? readBusinessOverviewComparisonProofBundles(debug)
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
? readCounterpartyValueFlowProofBundles(debug)
: null; : null;
const inheritedComparisonScope = state.session_context.comparison_scope; const inheritedComparisonScope = state.session_context.comparison_scope;
const inheritedComparisonProofBundles = const inheritedComparisonProofBundles =

View File

@ -1629,16 +1629,6 @@ function mergeFollowupFilters(
intent === "vat_payable_confirmed_as_of_date"; intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged); const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous); 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 currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
const currentContractExplicit = toNonEmptyString(merged.contract); const currentContractExplicit = toNonEmptyString(merged.contract);
const currentItemExplicit = toNonEmptyString(merged.item); const currentItemExplicit = toNonEmptyString(merged.item);
@ -1727,7 +1717,7 @@ function mergeFollowupFilters(
} }
if ( if (
(!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) && !currentHasPeriod &&
previousHasPeriod && previousHasPeriod &&
hasFollowupSignal && hasFollowupSignal &&
!hasExplicitPeriodInMessage && !hasExplicitPeriodInMessage &&

View File

@ -624,27 +624,12 @@ function normalizeCounterpartyCandidate(
toNonEmptyString: (value: unknown) => string | null toNonEmptyString: (value: unknown) => string | null
): string | null { ): string | null {
const text = toNonEmptyString(value); const text = toNonEmptyString(value);
if (!text || isReferentialCounterpartyPlaceholder(text) || isCounterpartyRoleFragment(text)) { if (!text || isReferentialCounterpartyPlaceholder(text)) {
return null; return null;
} }
return text; return text;
} }
function isCounterpartyRoleFragment(value: unknown): boolean {
const text = normalizeRussianComparableText(value);
if (!text) {
return false;
}
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
if (!hasRoleWord) {
return false;
}
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
}
function sameCounterpartyCandidate( function sameCounterpartyCandidate(
left: string | null, left: string | null,
right: string | null right: string | null
@ -892,9 +877,6 @@ export function resolveAddressDebugCarryoverFilters(
const preferGroundedDiscoveryCounterparty = const preferGroundedDiscoveryCounterparty =
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty); hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString); const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
if (!existingCounterparty) {
delete nextFilters.counterparty;
}
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) { if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
nextFilters.counterparty = counterparty; nextFilters.counterparty = counterparty;
} }
@ -1158,10 +1140,6 @@ export function resolveFollowupTargetIntent(
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean { export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint); const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
return ( 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_on_hand_as_of_date" ||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" || normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
normalizedIntent === "inventory_purchase_provenance_for_item" || normalizedIntent === "inventory_purchase_provenance_for_item" ||

View File

@ -281,7 +281,7 @@ function valueFlowDirectionLabelRu(pilot: AssistantMcpDiscoveryPilotExecutionCon
: "входящих денежных поступлений"; : "входящих денежных поступлений";
} }
function valueFlowZeroResultInsufficiencyLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (!hasExecutedZeroValueFlowRows(pilot)) { if (!hasExecutedZeroValueFlowRows(pilot)) {
return null; return null;
} }
@ -293,23 +293,7 @@ function valueFlowZeroResultInsufficiencyLine(pilot: AssistantMcpDiscoveryPilotE
const period = explicitDateScope(pilot); const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : ""; const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне"; const periodPart = period ? ` за период ${period}` : " в проверенном окне";
const direction = valueFlowDirectionLabelRu(pilot); return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(
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 pilot
)} не найдено.`; )} не найдено.`;
} }
@ -328,11 +312,11 @@ function valueFlowZeroResultUnknownLine(pilot: AssistantMcpDiscoveryPilotExecuti
} }
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot); const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
if (!insufficiencyLine) { if (!confirmedLine) {
return null; return null;
} }
return insufficiencyLine; return confirmedLine;
} }
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
@ -1113,13 +1097,6 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
} }
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном."; return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
} }
if (mode === "checked_sources_only" && hasExecutedZeroValueFlowRows(pilot)) {
const flowLabel =
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
? "исходящие платежи/списания"
: "входящие денежные поступления";
return `Можно следующим шагом проверить ${flowLabel} по этому контрагенту в соседних периодах, по договорам или документам, либо собрать двусторонний денежный поток, чтобы отделить отсутствие строк в текущем срезе от операций вне проверенного окна.`;
}
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) { if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
return pilot.derived_business_overview return pilot.derived_business_overview
? businessOverviewNextStepLine(pilot.derived_business_overview) ? businessOverviewNextStepLine(pilot.derived_business_overview)
@ -2142,6 +2119,8 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
? [derivedValueLine] ? [derivedValueLine]
: derivedValueLine : derivedValueLine
? [derivedValueLine, ...monthlyConfirmedLines] ? [derivedValueLine, ...monthlyConfirmedLines]
: valueFlowZeroResultConfirmedLine(pilot)
? [valueFlowZeroResultConfirmedLine(pilot)!]
: derivedEntityResolutionLine : derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine] ? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine : derivedMetadataLine
@ -2153,12 +2132,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
? pilot.derived_metadata_surface.available_fields.length > 0 ? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts) ? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."] : ["Детальный список полей этих объектов этим шагом не получен."]
: appendValueFlowZeroResultUnknown( : appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
valueFlowZeroResultCheckedLine(pilot)
? [valueFlowZeroResultCheckedLine(pilot)!, ...rankedValueFlowUnknownLines(pilot)]
: rankedValueFlowUnknownLines(pilot),
pilot
);
return { return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,

View File

@ -1,5 +1,4 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy"; import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
import { normalizeRussianComparableText } from "./addressTextRepair";
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
"assistant_data_need_graph_v1" as const; "assistant_data_need_graph_v1" as const;
@ -308,40 +307,6 @@ function hasDetailedBusinessOverviewMoneyBreakdownHint(rawUtterance: string): bo
); );
} }
function hasWideBusinessOverviewSurfaceHint(rawUtterance: string): boolean {
if (!rawUtterance) {
return false;
}
const textVariants = Array.from(new Set([rawUtterance, normalizeRussianComparableText(rawUtterance)]));
if (
textVariants.some((text) =>
/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0434\u0435\u043d\p{L}*)/iu.test(
text
)
)
) {
return false;
}
const hasBroadCue =
textVariants.some((text) =>
/(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]*\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\p{L}*\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0446\u0435\u043d\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|business\s+overview|business\s+review|company\s+overview)/iu.test(
text
)
);
if (!hasBroadCue) {
return false;
}
const surfaceMarkers = [
/(?:\u043d\u0434\u0441|vat|\u043d\u0430\u043b\u043e\u0433\p{L}*)/iu,
/(?:\u0434\u043e\u043b\u0433\p{L}*|\u0437\u0430\u0434\u043e\u043b\u0436\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440)/iu,
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442\u043a|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440\p{L}*)/iu,
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b)/iu,
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b)/iu,
/(?:\u0447\u0442\u043e\s+(?:\u043f\u043e\u043a\u0430\s+)?\u043d\u0435\u043b\u044c\u0437\u044f\s+\u0443\u0442\u0432\u0435\u0440\u0436\u0434|\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d|\u0433\u0440\u0430\u043d\u0438\u0446\p{L}*)/iu
];
return textVariants.some((text) => surfaceMarkers.filter((marker) => marker.test(text)).length >= 3);
}
function supportsOrganizationScopedOpenTotal(action: string): boolean { function supportsOrganizationScopedOpenTotal(action: string): boolean {
return action === "turnover" || action === "payout"; return action === "turnover" || action === "payout";
} }
@ -600,8 +565,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const rawAction = lower(turnMeaning?.asked_action_family); const rawAction = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family); const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance); const rawUtterance = lower(input.rawUtterance);
const rawQuestionSource = [input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "); const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
const rawQuestionSignal = lower(rawQuestionSource);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis); const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need); const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
@ -619,15 +583,12 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
}); });
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal); const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal); const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
const wideBusinessOverviewSurfaceHint = hasWideBusinessOverviewSurfaceHint(rawQuestionSource);
const oneSidedBusinessOverviewValueFlowOverride = const oneSidedBusinessOverviewValueFlowOverride =
initialBusinessFactFamily === "business_overview" && initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) && Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint; !detailedBusinessOverviewMoneyBreakdownHint;
const bidirectionalBusinessOverviewValueFlowOverride = const bidirectionalBusinessOverviewValueFlowOverride =
initialBusinessFactFamily === "business_overview" && initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal) &&
!wideBusinessOverviewSurfaceHint;
const oneSidedValueFlowActionOverride = const oneSidedValueFlowActionOverride =
initialBusinessFactFamily === "value_flow" && initialBusinessFactFamily === "value_flow" &&
Boolean(oneSidedValueAction) && Boolean(oneSidedValueAction) &&
@ -774,9 +735,6 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
if (businessFactFamily === "business_overview" && !explicitDateScope) { if (businessFactFamily === "business_overview" && !explicitDateScope) {
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope"); pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
} }
if (initialBusinessFactFamily === "business_overview" && wideBusinessOverviewSurfaceHint) {
pushReason(reasonCodes, "data_need_graph_business_overview_wide_surface_preserved");
}
if (directBusinessOverviewMoneyAnswerHint) { if (directBusinessOverviewMoneyAnswerHint) {
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer"); pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
} }

View File

@ -3217,28 +3217,9 @@ function deriveValueFlow(
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"], direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
): AssistantMcpDiscoveryDerivedValueFlow | null { ): AssistantMcpDiscoveryDerivedValueFlow | null {
if (!result || result.error) { if (!result || result.error || result.matched_rows <= 0) {
return null; 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 totalAmount = 0;
let rowsWithAmount = 0; let rowsWithAmount = 0;
for (const row of result.rows) { for (const row of result.rows) {
@ -5313,30 +5294,16 @@ function buildValueFlowConfirmedFacts(
counterparty: string | null, counterparty: string | null,
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"] direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
): string[] { ): string[] {
if (result.error) { if (result.error || result.matched_rows <= 0) {
return []; return [];
} }
if (direction === "outgoing_supplier_payout") { 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 [ return [
counterparty counterparty
? `1C supplier-payout rows were found for counterparty ${counterparty}` ? `1C supplier-payout rows were found for counterparty ${counterparty}`
: "1C supplier-payout rows were found for the requested counterparty scope" : "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 [ return [
counterparty counterparty
? `1C value-flow rows were found for counterparty ${counterparty}` ? `1C value-flow rows were found for counterparty ${counterparty}`
@ -5554,14 +5521,6 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
return []; return [];
} }
const facts: string[] = []; 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") { if (derived.value_flow_direction === "outgoing_supplier_payout") {
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"); facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
} else { } else {

View File

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

View File

@ -284,15 +284,9 @@ function localizeLine(value: string): string {
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) { if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе."; 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)) { if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе."; 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( 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 /^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
); );
@ -325,10 +319,6 @@ function localizeLine(value: string): string {
if (valueFlowMatch) { if (valueFlowMatch) {
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`; 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)) { if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру."; return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
} }
@ -350,10 +340,6 @@ function localizeLine(value: string): string {
if (supplierPayoutMatch) { if (supplierPayoutMatch) {
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`; 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)) { if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру."; return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
} }
@ -391,12 +377,6 @@ function localizeLine(value: string): string {
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) { if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С."; 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)) { if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С."; return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
} }
@ -858,9 +838,6 @@ function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
const comparable = compactComparable(text); const comparable = compactComparable(text);
if ( if (
/^(?:с|без)\s+разбивк/.test(comparable) || /^(?:с|без)\s+разбивк/.test(comparable) ||
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
comparable
) ||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable) /\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
) { ) {
return null; return null;
@ -1449,7 +1426,7 @@ function buildCompactBusinessOverviewReply(
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
].filter((value): value is string => Boolean(value)); ].filter((value): value is string => Boolean(value));
lines.push( lines.push(
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.` `По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`
); );
lines.push( lines.push(
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж." "Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
@ -1492,7 +1469,7 @@ function buildCompactBusinessOverviewReply(
} }
if (!limitLine) { if (!limitLine) {
lines.push( lines.push(
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли." "Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
); );
} }
const missingOverviewFamilies: string[] = []; const missingOverviewFamilies: string[] = [];

View File

@ -526,66 +526,6 @@ function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarificati
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed; return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
} }
function hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "clarification_candidate" && candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const separateCounterpartyCandidates = readStringArray(turnMeaning?.business_overview_separate_entity_candidates);
const hasPreviousCounterpartyBundle = Boolean(
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle) ||
toRecordObject(turnMeaning?.previous_counterparty_document_bundle)
);
const asksForBoundarySummary = Boolean(
(selectedChainId === "business_overview" ||
discoveryFamily === "business_overview" ||
askedDomain === "business_overview") &&
(separateCounterpartyCandidates.length > 0 || hasPreviousCounterpartyBundle)
);
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId =
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(
missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_"))
);
return asksForBoundarySummary && staleInventoryItemClarification;
}
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification( function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput, input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null, entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
@ -855,65 +795,6 @@ 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( function hasExactDocumentListAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput, input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@ -1358,9 +1239,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
entryPoint entryPoint
); );
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint); const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint); const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
const exactDocumentListAddressReply =
hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint); const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply( const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
input, input,
@ -1389,8 +1268,6 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
); );
const groundedMovementCandidatePriority = const groundedMovementCandidatePriority =
hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate); hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
const businessOverviewBoundaryCandidatePriority =
hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
const groundedValueFlowRankingCandidatePriority = const groundedValueFlowRankingCandidatePriority =
hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate); hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
const currentClarificationProtectsCurrentReply = const currentClarificationProtectsCurrentReply =
@ -1398,18 +1275,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!discoveryClarificationCandidatePriority && !discoveryClarificationCandidatePriority &&
!groundedValueFlowCandidatePriority && !groundedValueFlowCandidatePriority &&
!groundedMovementCandidatePriority && !groundedMovementCandidatePriority &&
!businessOverviewBoundaryCandidatePriority &&
!groundedValueFlowRankingCandidatePriority; !groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent = const alignedFactualAddressReplyProtectsCurrent =
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery; alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const matchedFactualAddressContinuationTargetProtectsCurrent = const matchedFactualAddressContinuationTargetProtectsCurrent =
matchedFactualAddressContinuationTarget && matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
!groundedValueFlowRankingCandidatePriority &&
!documentListTemporalConflictWithDiscovery;
const fullConfirmedFactualAddressReplyProtectsCurrent = const fullConfirmedFactualAddressReplyProtectsCurrent =
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery; fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactMatchedFactualAddressReplyProtectsCurrent = const exactMatchedFactualAddressReplyProtectsCurrent =
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery; exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactBankOperationsProtectsCurrent = const exactBankOperationsProtectsCurrent =
exactBankOperationsAddressReply && exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning && !semanticConflictWithDiscoveryTurnMeaning &&
@ -1446,9 +1320,6 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (evidenceLaneConflictWithDiscoveryTurnMeaning) { if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override"); 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) { if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply"); pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
} }
@ -1470,12 +1341,6 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
"mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification" "mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification"
); );
} }
if (businessOverviewBoundaryCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
);
}
if (groundedValueFlowRankingCandidatePriority) { if (groundedValueFlowRankingCandidatePriority) {
pushReason( pushReason(
reasonCodes, reasonCodes,

View File

@ -126,14 +126,6 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
if (isNegativeCounterpartyScopePlaceholder(value)) { if (isNegativeCounterpartyScopePlaceholder(value)) {
return true; return true;
} }
const text = compactLower(repairAddressMojibakeText(value));
if (
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
text
)
) {
return true;
}
return new Set([ return new Set([
"эта организация", "эта организация",
"этой организации", "этой организации",
@ -1266,12 +1258,6 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
) { ) {
return true; return true;
} }
if (
/(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(text) &&
hasPayoutSignal(text)
) {
return true;
}
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test( return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
text text
); );
@ -1334,7 +1320,7 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
if (!text) { if (!text) {
return null; return null;
} }
if (isReferentialOrganizationPlaceholder(text)) { if (isNegativeCounterpartyScopePlaceholder(text)) {
return null; return null;
} }
const normalized = text const normalized = text

View File

@ -202,10 +202,7 @@ function detectScopedCounterpartyEntity(text) {
if (!rawEntity) { if (!rawEntity) {
continue; continue;
} }
const entity = rawEntity const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
.replace(/^["'«»]+|["'«»]+$/gu, "")
.replace(/^(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043b\u0438\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?)\s+/iu, "")
.trim();
if (entity.length >= 2 && !ignored.has(entity)) { if (entity.length >= 2 && !ignored.has(entity)) {
return entity; return entity;
} }
@ -220,11 +217,7 @@ function detectCounterpartyBidirectionalValueFlowFamily(text) {
/(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text); /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
const hasOutgoingCue = const hasOutgoingCue =
/(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text); /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
const hasOverallMoneyMeaningCue = if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
/(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(
text
);
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue) || (hasOverallMoneyMeaningCue && hasOutgoingCue))) {
return null; return null;
} }
const entity = detectScopedCounterpartyEntity(text); const entity = detectScopedCounterpartyEntity(text);
@ -560,8 +553,6 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
const explicitIntentCandidate = const explicitIntentCandidate =
broadBusinessEvaluation?.family broadBusinessEvaluation?.family
? null ? null
: counterpartyBidirectionalValueFlow?.family
? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null); : supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family const unsupportedFamily = broadBusinessEvaluation?.family
? broadBusinessEvaluation.family ? broadBusinessEvaluation.family

View File

@ -164,28 +164,6 @@ describe("address follow-up temporal regressions", () => {
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror"); 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", () => { it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", { const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
previous_intent: "vat_payable_confirmed_as_of_date", previous_intent: "vat_payable_confirmed_as_of_date",
@ -476,29 +454,6 @@ describe("address follow-up temporal regressions", () => {
expect(movements?.baseReasons).toContain("counterparty_from_followup_context"); 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", () => { it("replaces pronoun chain anchors from counterparty follow-up context", () => {
const followupContext = { const followupContext = {
previous_intent: "customer_revenue_and_payments" as const, previous_intent: "customer_revenue_and_payments" as const,

View File

@ -132,16 +132,6 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected"); expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
}); });
it("routes agent margin-inspection follow-up to inventory margin instead of unknown", () => {
const result = resolveAddressIntent(
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
);
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result.intent).not.toBe("unknown");
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
});
it("detects bare historical inventory root with explicit month-year", () => { it("detects bare historical inventory root with explicit month-year", () => {
const result = resolveAddressIntent("остатки на март 2016"); const result = resolveAddressIntent("остатки на март 2016");

View File

@ -44,15 +44,6 @@ describe("addressInventoryIntentSignals", () => {
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature"); expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
}); });
it("classifies margin inspection wording from the phase106 replay as inventory margin", () => {
const result = resolveInventoryAddressIntent(
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
);
expect(result?.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.reasons).toContain("inventory_margin_ranking_signal_detected");
});
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => { it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
const result = resolveInventoryAddressIntent("selected object supplier provenance"); const result = resolveInventoryAddressIntent("selected object supplier provenance");

View File

@ -105,68 +105,6 @@ describe("address navigation state", () => {
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31"); expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
}); });
it("does not store broad money role wording as a counterparty focus object", () => {
const base = createEmptyAddressNavigationState("asst-bo-money", "2026-04-12T10:00:00.000Z");
const organization = 'ООО "Альтернатива Плюс"';
const assistantItem = {
message_id: "msg-bo-money",
session_id: "asst-bo-money",
role: "assistant",
text:
"По компании ООО \"Альтернатива Плюс\" за 2020 получили 47 628 853,03 руб.; крупнейший небанковский входящий контрагент: Группа СВК.",
reply_type: "partial_coverage",
created_at: "2026-04-12T10:11:00.000Z",
trace_id: "address-bo-money",
debug: {
detected_mode: "address_query",
detected_intent: "unknown",
selected_recipe: null,
extracted_filters: {
counterparty: "и главный поставщик в",
organization,
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "unknown",
mcp_discovery_response_applied: true,
mcp_discovery_selected_chain_id: "business_overview",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: organization,
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "business_overview",
subject_candidates: []
}
},
bridge: {
bridge_status: "answer_draft_ready",
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
},
dialog_continuation_contract_v2: {
decision: "continue_previous"
}
}
} as any;
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
expect(evolved.result_sets[0]?.filters.counterparty).toBeUndefined();
expect(evolved.result_sets[0]?.filters.organization).toBe(organization);
expect(evolved.session_context.active_focus_object?.object_type).toBe("organization");
expect(evolved.session_context.active_focus_object?.label).toBe(organization);
expect(evolved.navigation_history[0]?.target_object_id).toBe(`organization:${organization}`.toLowerCase());
});
it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => { it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => {
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z"); const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
const assistantItem = { const assistantItem = {
@ -220,262 +158,6 @@ describe("address navigation state", () => {
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31"); 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", () => { it("keeps selected counterparty focus for company boundary summaries", () => {
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z"); const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
const assistantItem = { const assistantItem = {

View File

@ -150,33 +150,6 @@ describe("assistantContinuityPolicy organization authority", () => {
}); });
}); });
it("does not expose broad money role fragments as counterparty carryover", () => {
const debug = {
execution_lane: "address_query",
detected_intent: "business_overview",
extracted_filters: {
counterparty: "и главный поставщик в",
organization: 'ООО "Альтернатива Плюс"',
period_from: "2020-01-01",
period_to: "2020-12-31"
},
anchor_type: "counterparty",
anchor_value_raw: "и главный поставщик в",
anchor_value_resolved: "и главный поставщик в"
};
expect(readAddressDebugCounterparty(debug)).toBeNull();
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
organization: 'ООО "Альтернатива Плюс"',
period_from: "2020-01-01",
period_to: "2020-12-31"
});
expect(resolveAddressDebugAnchorContext(debug)).toEqual({
anchorType: "counterparty",
anchorValue: null
});
});
it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => { it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => {
const debug = { const debug = {
execution_lane: "living_chat", execution_lane: "living_chat",
@ -684,27 +657,6 @@ 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", () => { it("resolves inventory root pivots through one shared helper", () => {
const flags = resolveInventoryFollowupPivotFlags( const flags = resolveInventoryFollowupPivotFlags(
{ intent: "inventory_on_hand_as_of_date" }, { intent: "inventory_on_hand_as_of_date" },

View File

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

View File

@ -1,11 +1,6 @@
import iconv from "iconv-lite";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph"; import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
function toWin1251Mojibake(value: string): string {
return iconv.decode(Buffer.from(value, "utf8"), "win1251");
}
describe("assistant MCP discovery data need graph", () => { describe("assistant MCP discovery data need graph", () => {
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => { it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({ const result = buildAssistantMcpDiscoveryDataNeedGraph({
@ -408,59 +403,6 @@ describe("assistant MCP discovery data need graph", () => {
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing"); expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
}); });
it("keeps a wide formal business overview on the business-overview graph even when money net is requested", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
rawUtterance:
"\u0414\u0430\u0439 \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020 \u0433\u043e\u0434 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438, \u043d\u0435\u0442\u0442\u043e, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0438, \u0441\u043a\u043b\u0430\u0434, \u043a\u043b\u0438\u0435\u043d\u0442\u044b, \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0438 \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0442\u044c.",
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_date_scope: "2020",
explicit_organization_scope:
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
}
});
expect(result.business_fact_family).toBe("business_overview");
expect(result.action_family).toBe("broad_evaluation");
expect(result.comparison_need).toBeNull();
expect(result.subject_candidates).toEqual([]);
expect(result.reason_codes).toContain("data_need_graph_business_overview_wide_surface_preserved");
expect(result.reason_codes).not.toContain(
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
);
});
it("keeps a wide formal business overview when the runtime signal is win1251 mojibake", () => {
const wideQuestion =
"\u0414\u0430\u0439 \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020 \u0433\u043e\u0434 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438, \u043d\u0435\u0442\u0442\u043e, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0438, \u0441\u043a\u043b\u0430\u0434, \u043a\u043b\u0438\u0435\u043d\u0442\u044b, \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0438 \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0442\u044c.";
const legacyQuestion = toWin1251Mojibake(wideQuestion);
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
rawUtterance: legacyQuestion,
turnMeaning: {
raw_message: legacyQuestion,
effective_message: legacyQuestion,
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_date_scope: "2020",
explicit_organization_scope:
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
}
});
expect(result.business_fact_family).toBe("business_overview");
expect(result.action_family).toBe("broad_evaluation");
expect(result.comparison_need).toBeNull();
expect(result.reason_codes).toContain("data_need_graph_business_overview_wide_surface_preserved");
expect(result.reason_codes).not.toContain(
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
);
});
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => { it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({ const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence", semanticDataNeed: "counterparty value-flow evidence",

View File

@ -1650,38 +1650,6 @@ describe("assistant MCP discovery pilot executor", () => {
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета"); 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 () => { it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {

View File

@ -1132,55 +1132,6 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("generic advice"); expect(candidate.reply_text).not.toContain("generic advice");
}); });
it("does not render negative next-check instruction as organization scope", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "generic advice should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("\u0412 \u043f\u0435\u0440\u0432\u0443\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
expect(candidate.reply_text).not.toContain("\u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439");
});
it("keeps counterparty leaders for money followups instead of compact cashflow", () => { it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ entryPoint({
@ -1602,9 +1553,8 @@ describe("assistant MCP discovery response candidate", () => {
expect(firstLine).toContain("По подтвержденным строкам 1С"); expect(firstLine).toContain("По подтвержденным строкам 1С");
expect(firstLine).toContain("входящие 157 192 981,43 руб."); expect(firstLine).toContain("входящие 157 192 981,43 руб.");
expect(firstLine).toContain("исходящие 35 439 044,74 руб."); 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("Что видно в ограниченном денежном срезе"); expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе"); expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");

View File

@ -525,177 +525,6 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply"); expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
}); });
it("uses a business-overview boundary clarification over a stale inventory-item clarification", () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 rub." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 rub." },
net_amount_human_ru: "18 523 839 rub.",
net_direction: "net_incoming"
};
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Need item before I can continue.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
detected_intent: "inventory_purchase_documents_for_item",
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
capability_id: "inventory_inventory_purchase_documents_for_item",
truth_mode: "clarification_required",
answer_shape: "clarification_required",
missing_required_filters: ["item"],
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage",
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
business_overview_separate_entity_candidates: ["Group SVK"],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
unsupported_but_understood_family: "broad_business_evaluation"
},
data_need_graph: {
business_fact_family: "business_overview",
action_family: "broad_evaluation",
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "Specify organization.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: "Specify organization."
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Group SVK");
expect(result.reply_text).toContain("20 653 490 rub.");
expect(result.reply_text).toContain("19");
expect(result.reply_text).not.toContain("Need item");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("uses a ready business-overview boundary answer over a stale inventory-item clarification", () => {
const valueFlowBundle = {
counterparty: "Group SVK",
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 rub." },
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 rub." },
net_amount_human_ru: "18 523 839 rub.",
net_direction: "net_incoming"
};
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Need item before I can continue.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
detected_intent: "inventory_purchase_documents_for_item",
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
capability_id: "inventory_inventory_purchase_documents_for_item",
truth_mode: "clarification_required",
answer_shape: "clarification_required",
missing_required_filters: ["item"],
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage",
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
hot_runtime_wired: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "Alternative Plus",
business_overview_separate_entity_candidates: ["Group SVK"],
previous_counterparty_value_flow_bundle: valueFlowBundle,
previous_counterparty_document_bundle: documentBundle,
unsupported_but_understood_family: "broad_business_evaluation"
},
data_need_graph: {
business_fact_family: "business_overview",
action_family: "broad_evaluation",
clarification_gaps: [],
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
execution_handoff: {
mcp_execution_performed: true,
can_use_guarded_response: true,
must_keep_internal_mechanics_hidden: true
},
pilot: {
pilot_scope: "business_overview_route_template_v1",
mcp_execution_performed: true,
derived_business_overview: {
period_scope: "all_time",
incoming_customer_revenue: { total_amount_human_ru: "285 819 547 rub." },
outgoing_supplier_payout: { total_amount_human_ru: "137 963 720 rub." },
net_amount_human_ru: "147 855 827 rub.",
net_direction: "net_incoming",
top_customers: [],
top_suppliers: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Company summary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Alternative Plus");
expect(result.reply_text).toContain("Group SVK");
expect(result.reply_text).toContain("20 653 490 rub.");
expect(result.reply_text).not.toContain("Need item");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => { it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({ const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.", currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
@ -757,72 +586,6 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply"); 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", () => { it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({ const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.", currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",

View File

@ -163,42 +163,6 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected"); expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
}); });
it("treats supplier payout plus overall money meaning as bidirectional value-flow discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"А теперь поставщику Группа СВК за 2020 сколько мы ему заплатили и какой общий денежный смысл?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: [{ value: "группа свк" }],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["группа свк"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
});
expect(result.data_need_graph).toMatchObject({
business_fact_family: "value_flow",
action_family: "net_value_flow",
comparison_need: "incoming_vs_outgoing"
});
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
});
it("keeps explicit counterparty money-flow basis questions executable without requiring a period", () => { it("keeps explicit counterparty money-flow basis questions executable without requiring a period", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "How much money passed with SVK, incoming and outgoing, and what documents or movements prove it?", userMessage: "How much money passed with SVK, incoming and outgoing, and what documents or movements prove it?",
@ -4112,39 +4076,4 @@ describe("assistant MCP discovery turn input adapter", () => {
); );
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty"); expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
}); });
it("uses retained organization scope instead of negative next-check instruction text", () => {
const orgName = "\u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\"";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
assistantTurnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: {
counterparty: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
}
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: { organization: orgName },
root_filters: { organization: orgName },
previous_anchor_type: "organization",
previous_anchor_value: orgName,
previous_intent: "business_overview",
root_intent: "business_overview"
}
});
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe(orgName);
expect(result.turn_meaning_ref?.explicit_organization_scope).not.toBe(
"\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
});
}); });

View File

@ -85,31 +85,6 @@ describe("assistantTurnMeaningPolicy", () => {
expect(meaning.stale_replay_forbidden).toBe(true); expect(meaning.stale_replay_forbidden).toBe(true);
}); });
it("treats payout plus overall money meaning as bidirectional value-flow", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "supplier_payouts_profile", confidence: "high" })
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0443 \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u044b \u0435\u043c\u0443 \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u0449\u0438\u0439 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u0441\u043c\u044b\u0441\u043b?"
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("counterparty_value");
expect(meaning.asked_action_family).toBe("net_value_flow");
expect(meaning.unsupported_but_understood_family).toBe("counterparty_bidirectional_value_flow_or_netting");
expect(meaning.explicit_entity_candidates).toEqual([
{
type: "counterparty",
value: "\u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u0432\u043a",
source: "current_turn_loose_entity_tail"
}
]);
expect(meaning.reason_codes).toContain("counterparty_bidirectional_value_flow_current_turn_signal");
expect(meaning.stale_replay_forbidden).toBe(true);
});
it("ignores temporal tail words in all-time revenue ranking questions", () => { it("ignores temporal tail words in all-time revenue ranking questions", () => {
const policy = buildPolicy({ const policy = buildPolicy({
resolveAddressIntent: (text: string) => resolveAddressIntent: (text: string) =>

View File

@ -1,170 +1,4 @@
[ [
{
"generation_id": "gen-ag06151952-5d8f12",
"created_at": "2026-06-15T19:52:40+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase107 mixed planner brain dogfood",
"count": 9,
"domain": "address_phase107_mixed_planner_brain_dogfood",
"questions": [
"привет, ты на связи?",
"найди в 1С контрагента СВК",
"сколько получили по нему за 2020 год",
"а теперь сколько заплатили?",
"а какое нетто?",
"а по документам?",
"а по движениям?",
"Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?",
"а чем капибара отличается от утки?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615195240_gen-ag06151952-5d8f12.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260615195240_gen-ag06151952-5d8f12.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "mixed planner autonomy and counterparty evidence carryover",
"architecture_phase": "assistant-stage1 phase107",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"alias_grounding",
"broad_business_evaluation",
"context_boundary",
"context_switch_from_counterparty_to_company",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_summary",
"human_answer",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"off_domain_living_chat",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_pivot"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06151746-7d3a57",
"created_at": "2026-06-15T17:46:11+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase106 profit cashflow margin next checks",
"count": 5,
"domain": "address_phase106_profit_cashflow_margin_next_step",
"questions": [
"ООО Альтернатива Плюс",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?",
"тогда какую прибыль или маржу можно проверить по товарам за 2020?",
"что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615174611_gen-ag06151746-7d3a57.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260615174611_gen-ag06151746-7d3a57.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Profit/cashflow boundary, inventory margin pivot, organization carryover, and concrete next-check plan after accepted phase106 p03 live replay.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"bare_org_scope",
"business_next_step",
"business_overview",
"cashflow_not_profit",
"cashflow_to_margin_pivot",
"colloquial_cashflow",
"company_scope",
"evidence_plan",
"followup_boundary",
"inventory",
"margin_profitability",
"no_generic_advice",
"profitability_boundary",
"wrong_domain_trap"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06151720-c4b54f",
"created_at": "2026-06-15T17:20:44+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
"count": 4,
"domain": "address_phase104_generic_role_tail_anchor_hygiene",
"questions": [
"ООО Альтернатива Плюс",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615172044_gen-ag06151720-c4b54f.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260615172044_gen-ag06151720-c4b54f.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Role-tail anchor hygiene, bank boundary, and supplier-prefix bidirectional money-flow after live domain-case-loop p04.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"bank_like_counterparty",
"bare_org_scope",
"business_overview",
"company_scope",
"counterparty_net_cash_flow",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"post_overview_anchor_integrity",
"stale_scope_guard",
"supplier_prefix_canary"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"saved_after_validated_replay": true
}
},
{ {
"generation_id": "gen-ag06031328-3e1dd1", "generation_id": "gen-ag06031328-3e1dd1",
"created_at": "2026-06-03T13:28:09+00:00", "created_at": "2026-06-03T13:28:09+00:00",

View File

@ -1,171 +0,0 @@
{
"saved_at": "2026-06-15T17:20:44+00:00",
"generation_id": "gen-ag06151720-c4b54f",
"mode": "saved_user_sessions",
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
"agent_run": true,
"questions": [
"ООО Альтернатива Плюс",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Role-tail anchor hygiene, bank boundary, and supplier-prefix bidirectional money-flow after live domain-case-loop p04.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"bank_like_counterparty",
"bare_org_scope",
"business_overview",
"company_scope",
"counterparty_net_cash_flow",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"post_overview_anchor_integrity",
"stale_scope_guard",
"supplier_prefix_canary"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"final_status": "accepted",
"loop_id": "phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 91,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "bbb3620956f85aa05bb0c2b76c109cb05a066185",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "ООО Альтернатива Плюс",
"created_at": "2026-06-15T17:20:44+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"created_at": "2026-06-15T17:20:44+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"created_at": "2026-06-15T17:20:44+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
"created_at": "2026-06-15T17:20:44+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Role-tail anchor hygiene, bank boundary, and supplier-prefix bidirectional money-flow after live domain-case-loop p04.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"bank_like_counterparty",
"bare_org_scope",
"business_overview",
"company_scope",
"counterparty_net_cash_flow",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"post_overview_anchor_integrity",
"stale_scope_guard",
"supplier_prefix_canary"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"final_status": "accepted",
"loop_id": "phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 91,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "bbb3620956f85aa05bb0c2b76c109cb05a066185",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,189 +0,0 @@
{
"saved_at": "2026-06-15T17:46:11+00:00",
"generation_id": "gen-ag06151746-7d3a57",
"mode": "saved_user_sessions",
"title": "AGENT | Phase106 profit cashflow margin next checks",
"agent_run": true,
"questions": [
"ООО Альтернатива Плюс",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?",
"тогда какую прибыль или маржу можно проверить по товарам за 2020?",
"что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Profit/cashflow boundary, inventory margin pivot, organization carryover, and concrete next-check plan after accepted phase106 p03 live replay.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"bare_org_scope",
"business_next_step",
"business_overview",
"cashflow_not_profit",
"cashflow_to_margin_pivot",
"colloquial_cashflow",
"company_scope",
"evidence_plan",
"followup_boundary",
"inventory",
"margin_profitability",
"no_generic_advice",
"profitability_boundary",
"wrong_domain_trap"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"final_status": "accepted",
"loop_id": "phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 92,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "d0d7962b65ea699bc5b6dfc82308efaf08d513e7",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "ООО Альтернатива Плюс",
"created_at": "2026-06-15T17:46:11+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "скока денег альтернатива заработала за 20 год?",
"created_at": "2026-06-15T17:46:11+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "а это чистая прибыль?",
"created_at": "2026-06-15T17:46:11+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "тогда какую прибыль или маржу можно проверить по товарам за 2020?",
"created_at": "2026-06-15T17:46:11+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?",
"created_at": "2026-06-15T17:46:11+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Profit/cashflow boundary, inventory margin pivot, organization carryover, and concrete next-check plan after accepted phase106 p03 live replay.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"bare_org_scope",
"business_next_step",
"business_overview",
"cashflow_not_profit",
"cashflow_to_margin_pivot",
"colloquial_cashflow",
"company_scope",
"evidence_plan",
"followup_boundary",
"inventory",
"margin_profitability",
"no_generic_advice",
"profitability_boundary",
"wrong_domain_trap"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"final_status": "accepted",
"loop_id": "phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 92,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "d0d7962b65ea699bc5b6dfc82308efaf08d513e7",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,245 +0,0 @@
{
"saved_at": "2026-06-15T19:52:40+00:00",
"generation_id": "gen-ag06151952-5d8f12",
"mode": "saved_user_sessions",
"title": "AGENT | Phase107 mixed planner brain dogfood",
"agent_run": true,
"questions": [
"привет, ты на связи?",
"найди в 1С контрагента СВК",
"сколько получили по нему за 2020 год",
"а теперь сколько заплатили?",
"а какое нетто?",
"а по документам?",
"а по движениям?",
"Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?",
"а чем капибара отличается от утки?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "mixed planner autonomy and counterparty evidence carryover",
"architecture_phase": "assistant-stage1 phase107",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"alias_grounding",
"broad_business_evaluation",
"context_boundary",
"context_switch_from_counterparty_to_company",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_summary",
"human_answer",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"off_domain_living_chat",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_pivot"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"final_status": "accepted",
"loop_id": "phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 91,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "7e70ec1d464548a9f422e35b717834211f8d7683",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "привет, ты на связи?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "найди в 1С контрагента СВК",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "сколько получили по нему за 2020 год",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а теперь сколько заплатили?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а какое нетто?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а по документам?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "а по движениям?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "а чем капибара отличается от утки?",
"created_at": "2026-06-15T19:52:40+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "mixed planner autonomy and counterparty evidence carryover",
"architecture_phase": "assistant-stage1 phase107",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"alias_grounding",
"broad_business_evaluation",
"context_boundary",
"context_switch_from_counterparty_to_company",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_summary",
"human_answer",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"off_domain_living_chat",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_pivot"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"final_status": "accepted",
"loop_id": "phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 91,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "7e70ec1d464548a9f422e35b717834211f8d7683",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -1,37 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag06151720-c4b54f",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-15T17:20:44+00:00",
"generation_id": "gen-ag06151720-c4b54f",
"mode": "saved_user_sessions",
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
"domain": "address_phase104_generic_role_tail_anchor_hygiene",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "ООО Альтернатива Плюс"
},
{
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик."
},
{
"user_message": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам."
},
{
"user_message": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
}
]
}
]
}

View File

@ -1,40 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag06151746-7d3a57",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-15T17:46:11+00:00",
"generation_id": "gen-ag06151746-7d3a57",
"mode": "saved_user_sessions",
"title": "AGENT | Phase106 profit cashflow margin next checks",
"domain": "address_phase106_profit_cashflow_margin_next_step",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase106 profit cashflow margin next checks",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "ООО Альтернатива Плюс"
},
{
"user_message": "скока денег альтернатива заработала за 20 год?"
},
{
"user_message": "а это чистая прибыль?"
},
{
"user_message": "тогда какую прибыль или маржу можно проверить по товарам за 2020?"
},
{
"user_message": "что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?"
}
]
}
]
}

View File

@ -1,52 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag06151952-5d8f12",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-15T19:52:40+00:00",
"generation_id": "gen-ag06151952-5d8f12",
"mode": "saved_user_sessions",
"title": "AGENT | Phase107 mixed planner brain dogfood",
"domain": "address_phase107_mixed_planner_brain_dogfood",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase107 mixed planner brain dogfood",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет, ты на связи?"
},
{
"user_message": "найди в 1С контрагента СВК"
},
{
"user_message": "сколько получили по нему за 2020 год"
},
{
"user_message": "а теперь сколько заплатили?"
},
{
"user_message": "а какое нетто?"
},
{
"user_message": "а по документам?"
},
{
"user_message": "а по движениям?"
},
{
"user_message": "Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?"
},
{
"user_message": "а чем капибара отличается от утки?"
}
]
}
]
}

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os
import re import re
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@ -14,40 +13,10 @@ DETECTOR_REGISTRY_PATH = REPO_ROOT / "docs" / "orchestration" / "detector_regist
ISSUE_CATALOG_PATH = REPO_ROOT / "docs" / "orchestration" / "issue_catalog.json" ISSUE_CATALOG_PATH = REPO_ROOT / "docs" / "orchestration" / "issue_catalog.json"
DEFAULT_GLOBAL_DETECTORS = ["missing_effective_runtime_json"] DEFAULT_GLOBAL_DETECTORS = ["missing_effective_runtime_json"]
DETECTOR_RESULTS_SCHEMA_VERSION = "agent_detector_results_v1" DETECTOR_RESULTS_SCHEMA_VERSION = "agent_detector_results_v1"
DEFAULT_LIMITED_NEXT_ACTION_EXTRA_PATTERNS = [
r"(?i)(что\s+проверить\s+дальше|для\s+ответа.{0,120}нужно|нужно\s+отдельно\s+(?:считать|посчитать|проверить)|проверить\s+дальше)"
]
def path_for_io(path: Path) -> str:
if os.name != "nt":
return str(path)
raw_value = str(path)
if raw_value.startswith("\\\\?\\"):
return raw_value
absolute_path = path if path.is_absolute() else Path.cwd() / path
absolute_value = str(absolute_path.absolute())
if absolute_value.startswith("\\\\?\\"):
return absolute_value
if absolute_value.startswith("\\\\"):
return "\\\\?\\UNC\\" + absolute_value.lstrip("\\")
return "\\\\?\\" + absolute_value
def path_exists(path: Path) -> bool:
try:
return os.path.exists(path_for_io(path))
except OSError:
return False
def read_text(path: Path) -> str:
with open(path_for_io(path), "r", encoding="utf-8") as handle:
return handle.read()
def read_json(path: Path) -> Any: def read_json(path: Path) -> Any:
return json.loads(read_text(path)) return json.loads(path.read_text(encoding="utf-8"))
def read_json_object(path: Path) -> dict[str, Any]: def read_json_object(path: Path) -> dict[str, Any]:
@ -59,9 +28,8 @@ def read_json_object(path: Path) -> dict[str, Any]:
def write_json(path: Path, payload: Any) -> None: def write_json(path: Path, payload: Any) -> None:
os.makedirs(path_for_io(path.parent), exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
with open(path_for_io(path), "w", encoding="utf-8") as handle: path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
handle.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
def utc_now() -> str: def utc_now() -> str:
@ -193,50 +161,13 @@ def select_detectors(
def read_text_or_empty(path: Path) -> str: def read_text_or_empty(path: Path) -> str:
try: try:
return read_text(path) return path.read_text(encoding="utf-8")
except OSError: except OSError:
return "" return ""
def decode_win1251_utf8_mojibake(value: str) -> str | None:
try:
return value.encode("cp1251").decode("utf-8")
except UnicodeError:
return None
def decode_latin1_utf8_mojibake(value: str) -> str | None:
try:
return value.encode("latin1").decode("utf-8")
except UnicodeError:
return None
def text_match_variants(value: str) -> list[str]:
variants: list[str] = []
queue = [str(value or "")]
while queue:
current = queue.pop(0)
if current in variants:
continue
variants.append(current)
for decoded in (decode_win1251_utf8_mojibake(current), decode_latin1_utf8_mojibake(current)):
if decoded and decoded not in variants and decoded not in queue:
queue.append(decoded)
return variants
def first_pattern_search(patterns: list[re.Pattern[str]], text: str) -> re.Match[str] | None:
for variant in text_match_variants(text):
for pattern in patterns:
match = pattern.search(variant)
if match:
return match
return None
def assistant_text_from_turn_path(path: Path | None) -> str: def assistant_text_from_turn_path(path: Path | None) -> str:
if path is None or not path_exists(path): if path is None or not path.exists():
return "" return ""
payload = read_json_object(path) payload = read_json_object(path)
assistant_message = payload.get("assistant_message") if isinstance(payload.get("assistant_message"), dict) else {} assistant_message = payload.get("assistant_message") if isinstance(payload.get("assistant_message"), dict) else {}
@ -293,21 +224,14 @@ def output_turn_path(output_path: Path) -> Path | None:
name = output_path.name name = output_path.name
if name == "output.md": if name == "output.md":
candidate = output_path.with_name("turn.json") candidate = output_path.with_name("turn.json")
return candidate if path_exists(candidate) else None return candidate if candidate.exists() else None
if name.endswith("_output.md"): if name.endswith("_output.md"):
prefix = name[: -len("_output.md")] prefix = name[: -len("_output.md")]
candidate = output_path.with_name(f"{prefix}_turn.json") candidate = output_path.with_name(f"{prefix}_turn.json")
return candidate if path_exists(candidate) else None return candidate if candidate.exists() else None
return None return None
def output_step_state_path(output_path: Path) -> Path | None:
if output_path.name != "output.md":
return None
candidate = output_path.with_name("step_state.json")
return candidate if path_exists(candidate) else None
def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]: def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
outputs: list[dict[str, Any]] = [] outputs: list[dict[str, Any]] = []
seen: set[Path] = set() seen: set[Path] = set()
@ -328,7 +252,6 @@ def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
"artifact_path": str(path.relative_to(artifact_dir)), "artifact_path": str(path.relative_to(artifact_dir)),
"text": read_text_or_empty(path), "text": read_text_or_empty(path),
"turn_path": turn_path, "turn_path": turn_path,
"step_state_path": output_step_state_path(path),
} }
) )
return outputs return outputs
@ -461,7 +384,7 @@ def evaluate_forbidden_regex(
lines = [line for line in text.splitlines() if line.strip()] lines = [line for line in text.splitlines() if line.strip()]
text = "\n".join(lines[:prefix_line_count]) text = "\n".join(lines[:prefix_line_count])
for pattern in patterns: for pattern in patterns:
match = first_pattern_search([pattern], text) match = pattern.search(text)
if match: if match:
evidence.append( evidence.append(
{ {
@ -488,7 +411,7 @@ def evaluate_required_any(
matched: list[dict[str, Any]] = [] matched: list[dict[str, Any]] = []
for output in outputs: for output in outputs:
text = current_answer_text_for_output(output) text = current_answer_text_for_output(output)
match = first_pattern_search(patterns, text) match = next((pattern.search(text) for pattern in patterns if pattern.search(text)), None)
if match: if match:
matched.append({"path": output["repo_path"], "match": match.group(0)[:240]}) matched.append({"path": output["repo_path"], "match": match.group(0)[:240]})
else: else:
@ -505,24 +428,18 @@ def evaluate_limited_next_action(
) -> dict[str, Any]: ) -> dict[str, Any]:
check = detector.get("check") if isinstance(detector.get("check"), dict) else {} check = detector.get("check") if isinstance(detector.get("check"), dict) else {}
limited_patterns = compile_patterns(normalize_string_list(check.get("limited_patterns"))) limited_patterns = compile_patterns(normalize_string_list(check.get("limited_patterns")))
next_action_pattern_sources = [ next_action_patterns = compile_patterns(normalize_string_list(check.get("required_next_action_patterns_any")))
*normalize_string_list(check.get("required_next_action_patterns_any")),
*normalize_string_list(check.get("required_next_action_patterns_extra_any")),
]
if detector_name == "limited_answer_without_next_action":
next_action_pattern_sources.extend(DEFAULT_LIMITED_NEXT_ACTION_EXTRA_PATTERNS)
next_action_patterns = compile_patterns(next_action_pattern_sources)
if not outputs: if not outputs:
return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope") return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope")
failures: list[dict[str, Any]] = [] failures: list[dict[str, Any]] = []
limited_outputs = 0 limited_outputs = 0
for output in outputs: for output in outputs:
text = current_answer_text_for_output(output) text = current_answer_text_for_output(output)
limited_match = first_pattern_search(limited_patterns, text) limited_match = next((pattern.search(text) for pattern in limited_patterns if pattern.search(text)), None)
if not limited_match: if not limited_match:
continue continue
limited_outputs += 1 limited_outputs += 1
if not first_pattern_search(next_action_patterns, text): if not any(pattern.search(text) for pattern in next_action_patterns):
failures.append({"path": output["repo_path"], "limited_match": limited_match.group(0)[:240]}) failures.append({"path": output["repo_path"], "limited_match": limited_match.group(0)[:240]})
status = "fail" if failures else "pass" status = "fail" if failures else "pass"
if failures: if failures:
@ -534,52 +451,6 @@ def evaluate_limited_next_action(
return build_result(detector_name, detector, status, message, evidence=failures) return build_result(detector_name, detector, status, message, evidence=failures)
def evaluate_answer_text_shape(
detector_name: str,
detector: dict[str, Any],
outputs: list[dict[str, Any]],
) -> dict[str, Any]:
if not outputs:
return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope")
failures: list[dict[str, Any]] = []
reviewed: list[dict[str, Any]] = []
unknown: list[dict[str, Any]] = []
for output in outputs:
step_state_path = output.get("step_state_path")
step_state = read_json_object(step_state_path) if isinstance(step_state_path, Path) else {}
review = step_state.get("business_first_review") if isinstance(step_state.get("business_first_review"), dict) else {}
direct_answer_first_ok = review.get("direct_answer_first_ok")
if direct_answer_first_ok is True:
reviewed.append({"path": output["repo_path"], "direct_answer_first_ok": True})
elif direct_answer_first_ok is False:
failures.append({"path": output["repo_path"], "direct_answer_first_ok": False})
else:
unknown.append({"path": output["repo_path"], "reason": "business_first_review_missing"})
if failures:
return build_result(
detector_name,
detector,
"fail",
"first-line direct answer check failed",
evidence=[*failures, *unknown],
)
if unknown:
return build_result(
detector_name,
detector,
"review",
"direct-answer shape requires business review",
evidence=[*reviewed, *unknown],
)
return build_result(
detector_name,
detector,
"pass",
"business-first step reviews confirm direct-answer-first shape",
evidence=reviewed,
)
def evaluate_trace_guard( def evaluate_trace_guard(
detector_name: str, detector_name: str,
detector: dict[str, Any], detector: dict[str, Any],
@ -769,7 +640,7 @@ def evaluate_detector(
): ):
return evaluate_composite(detector_name, detector, results_by_name) return evaluate_composite(detector_name, detector, results_by_name)
if kind == "answer_text_shape": if kind == "answer_text_shape":
return evaluate_answer_text_shape(detector_name, detector, scoped_outputs) return evaluate_manual_review(detector_name, detector, "direct-answer shape requires business review")
return build_result(detector_name, detector, "skipped", f"detector kind is not executable yet: {kind}") return build_result(detector_name, detector, "skipped", f"detector kind is not executable yet: {kind}")

View File

@ -317,7 +317,6 @@ DEFAULT_INVARIANT_SEVERITY: dict[str, str] = {
"forbidden_recipe_selected": "P0", "forbidden_recipe_selected": "P0",
"focus_object_missing": "P0", "focus_object_missing": "P0",
"wrong_date_scope_state": "P0", "wrong_date_scope_state": "P0",
"out_of_window_date_in_answer": "P0",
"direct_answer_missing": "P0", "direct_answer_missing": "P0",
"top_level_noise_present": "P0", "top_level_noise_present": "P0",
"business_direct_answer_missing": "P0", "business_direct_answer_missing": "P0",
@ -931,40 +930,6 @@ def first_non_empty_lines(text: str, limit: int = 3) -> list[str]:
return output return output
def expected_single_year_from_date_scope(date_scope: Any) -> str | None:
if not isinstance(date_scope, dict):
return None
if str(date_scope.get("scope") or "").strip().lower() == "all_time":
return None
period_from = normalize_iso_date(date_scope.get("period_from"))
period_to = normalize_iso_date(date_scope.get("period_to"))
if not period_from or not period_to:
return None
from_year = period_from[:4]
to_year = period_to[:4]
if from_year != to_year:
return None
return from_year if re.fullmatch(r"(?:19|20)\d{2}", from_year) else None
def dated_evidence_years_from_answer(text: Any) -> list[str]:
source = repair_text_mojibake(str(text or ""))
years: list[str] = []
for match in re.finditer(r"\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)", source):
years.append(match.group(1))
for match in re.finditer(r"\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b", source):
years.append(match.group(1))
return list(dict.fromkeys(years))
def answer_has_out_of_window_dates_for_scope(text: Any, date_scope: Any) -> bool:
expected_year = expected_single_year_from_date_scope(date_scope)
if not expected_year:
return False
years = dated_evidence_years_from_answer(text)
return any(year != expected_year for year in years)
def build_node_contract_index(raw_contract: dict[str, Any]) -> dict[str, dict[str, Any]]: def build_node_contract_index(raw_contract: dict[str, Any]) -> dict[str, dict[str, Any]]:
scenario_tree = raw_contract.get("scenario_tree") scenario_tree = raw_contract.get("scenario_tree")
if not isinstance(scenario_tree, dict): if not isinstance(scenario_tree, dict):
@ -1730,47 +1695,6 @@ def resolve_question_template(question_template: str, scenario_state: dict[str,
return pattern.sub(replace, question_template).strip() return pattern.sub(replace, question_template).strip()
def resolve_scenario_contract_placeholders(value: Any, scenario_state: dict[str, Any]) -> Any:
if isinstance(value, str):
runtime_resolved = str(resolve_runtime_placeholders(value))
if "{{" not in runtime_resolved:
return runtime_resolved
pattern = re.compile(r"{{\s*([^{}]+?)\s*}}")
def replace(match: re.Match[str]) -> str:
value = lookup_placeholder_value(match.group(1), scenario_state)
if isinstance(value, (dict, list)):
return dump_json(value)
return str(value)
return pattern.sub(replace, runtime_resolved)
if isinstance(value, list):
return [resolve_scenario_contract_placeholders(item, scenario_state) for item in value]
if isinstance(value, dict):
return {
key: resolve_scenario_contract_placeholders(item, scenario_state)
for key, item in value.items()
}
return value
def resolve_step_contract_placeholders(step: dict[str, Any], scenario_state: dict[str, Any]) -> dict[str, Any]:
resolved_step = dict(step)
resolved_step["required_filters"] = normalize_validation_filters(
resolve_scenario_contract_placeholders(step.get("required_filters") or {}, scenario_state)
)
for pattern_field in (
"forbidden_answer_patterns",
"required_answer_patterns_any",
"required_answer_patterns_all",
):
if pattern_field in step:
resolved_step[pattern_field] = normalize_pattern_list(
resolve_scenario_contract_placeholders(step.get(pattern_field) or [], scenario_state)
)
return resolved_step
def build_failed_step_state( def build_failed_step_state(
*, *,
scenario_id: str, scenario_id: str,
@ -2389,9 +2313,6 @@ def step_intent_matches_expected(state: dict[str, Any], expected_intents: list[s
observed_intents = [ observed_intents = [
str(state.get("detected_intent") or "").strip(), str(state.get("detected_intent") or "").strip(),
*normalize_string_list(state.get("mcp_discovery_effective_intents")), *normalize_string_list(state.get("mcp_discovery_effective_intents")),
str(state.get("mcp_discovery_selected_chain_id") or "").strip(),
str(state.get("mcp_discovery_route_candidate_fact_family") or "").strip(),
str(state.get("mcp_discovery_route_candidate_action_family") or "").strip(),
] ]
return any(identifier_in_list(intent, expected_intents) for intent in observed_intents if intent) return any(identifier_in_list(intent, expected_intents) for intent in observed_intents if intent)
@ -2630,8 +2551,6 @@ def is_validated_memory_checkpoint_answer(
) -> bool: ) -> bool:
if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations): if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations):
return True return True
if is_validated_bare_organization_scope_checkpoint(state, business_review, violations):
return True
tags = set(normalize_string_list(state.get("semantic_tags"))) tags = set(normalize_string_list(state.get("semantic_tags")))
if "memory" not in tags: if "memory" not in tags:
return False return False
@ -2647,39 +2566,6 @@ def is_validated_memory_checkpoint_answer(
) )
def is_validated_bare_organization_scope_checkpoint(
state: dict[str, Any],
business_review: dict[str, Any],
violations: list[str],
) -> bool:
if violations:
return False
tags = set(normalize_string_list(state.get("semantic_tags")))
if not tags.intersection({"bare_org_scope", "company_scope", "company_selected"}):
return False
if str(state.get("living_chat_response_source") or "").strip() != "deterministic_data_scope_selection_contract":
return False
if str(state.get("living_chat_data_scope_probe_status") or "").strip() != "resolved":
return False
selected_organization = first_non_empty_string(
state.get("living_chat_selected_organization"),
state.get("assistant_active_organization"),
)
if not selected_organization:
return False
assistant_text = str(state.get("assistant_text") or "")
normalized_answer = normalize_entity_filter_value_for_comparison(assistant_text)
normalized_organization = normalize_entity_filter_value_for_comparison(selected_organization)
if not normalized_organization or normalized_organization not in normalized_answer:
return False
return (
business_review.get("business_usefulness_ok") is True
and business_review.get("direct_answer_first_ok") is True
and business_review.get("answer_layering_ok") is True
and business_review.get("technical_garbage_present") is False
)
def is_validated_comparison_boundary_memory_checkpoint( def is_validated_comparison_boundary_memory_checkpoint(
state: dict[str, Any], state: dict[str, Any],
business_review: dict[str, Any], business_review: dict[str, Any],
@ -2902,12 +2788,7 @@ def is_validated_clean_meta_chat_answer(
return False return False
semantic_tags = set(normalize_string_list(state.get("semantic_tags"))) semantic_tags = set(normalize_string_list(state.get("semantic_tags")))
allowed_tags = { allowed_tags = {
"human_answer",
"meta_smalltalk", "meta_smalltalk",
"mcp_discovery_gate_sanity",
"off_domain_living_chat",
"stale_replay_forbidden",
"context_boundary",
"company_selected", "company_selected",
"organization_authority", "organization_authority",
"meta_capability", "meta_capability",
@ -3152,10 +3033,6 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
if current_date_scope and current_date_scope != required_filters["as_of_date"]: if current_date_scope and current_date_scope != required_filters["as_of_date"]:
violated_invariants.append("wrong_date_scope_state") violated_invariants.append("wrong_date_scope_state")
if answer_has_out_of_window_dates_for_scope(assistant_text, date_scope):
violated_invariants.append("out_of_window_date_in_answer")
warnings.append("out_of_window_date_in_answer")
if should_require_direct_answer(state): if should_require_direct_answer(state):
if not actual_direct_answer or is_top_level_noise_line(actual_direct_answer): if not actual_direct_answer or is_top_level_noise_line(actual_direct_answer):
violated_invariants.append("direct_answer_missing") violated_invariants.append("direct_answer_missing")
@ -3237,10 +3114,8 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
state["clarification_answer_validated"] = clarification_validated state["clarification_answer_validated"] = clarification_validated
state["missing_axis_clarification_validated"] = missing_axis_clarification_validated state["missing_axis_clarification_validated"] = missing_axis_clarification_validated
state["clean_meta_chat_answer_validated"] = clean_meta_chat_validated state["clean_meta_chat_answer_validated"] = clean_meta_chat_validated
effective_execution_status = "exact" if clean_meta_chat_validated else execution_status
state["execution_status"] = effective_execution_status
state["acceptance_status"] = acceptance_status_from_execution( state["acceptance_status"] = acceptance_status_from_execution(
effective_execution_status, execution_status,
hard_fail, hard_fail,
( (
bounded_validated bounded_validated
@ -3360,10 +3235,6 @@ def build_scenario_step_state(
"detected_intent": debug.get("detected_intent"), "detected_intent": debug.get("detected_intent"),
"selected_recipe": debug.get("selected_recipe"), "selected_recipe": debug.get("selected_recipe"),
"capability_id": debug.get("capability_id"), "capability_id": debug.get("capability_id"),
"living_chat_response_source": debug.get("living_chat_response_source"),
"living_chat_data_scope_probe_status": debug.get("living_chat_data_scope_probe_status"),
"living_chat_selected_organization": debug.get("living_chat_selected_organization"),
"assistant_active_organization": debug.get("assistant_active_organization"),
"capability_contract_id": first_non_empty_string( "capability_contract_id": first_non_empty_string(
debug.get("capability_contract_id"), debug.get("capability_contract_id"),
runtime_contract.get("capability_contract_id"), runtime_contract.get("capability_contract_id"),
@ -3729,7 +3600,6 @@ def execute_scenario_manifest(
step_dir = steps_dir / step["step_id"] step_dir = steps_dir / step["step_id"]
try: try:
resolved_question = resolve_question_template(step["question_template"], scenario_state) resolved_question = resolve_question_template(step["question_template"], scenario_state)
resolved_step = resolve_step_contract_placeholders(step, scenario_state)
step_analysis_context = merge_analysis_context(manifest.get("analysis_context"), step.get("analysis_context")) step_analysis_context = merge_analysis_context(manifest.get("analysis_context"), step.get("analysis_context"))
step_analysis_context = carry_forward_analysis_context( step_analysis_context = carry_forward_analysis_context(
scenario_state, scenario_state,
@ -3741,7 +3611,7 @@ def execute_scenario_manifest(
args=args, args=args,
domain=manifest["domain"], domain=manifest["domain"],
scenario_id=manifest["scenario_id"], scenario_id=manifest["scenario_id"],
step=resolved_step, step=step,
step_index=step_index, step_index=step_index,
session_id=scenario_state.get("session_id"), session_id=scenario_state.get("session_id"),
question_resolved=resolved_question, question_resolved=resolved_question,
@ -6355,22 +6225,7 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
return names[:limit] return names[:limit]
status = str(summary.get("status") or "skipped") status = str(summary.get("status") or "skipped")
declared_detectors = normalize_string_list(detector_results.get("declared_detectors_under_test")) declared_detector_count = len(normalize_string_list(detector_results.get("declared_detectors_under_test")))
declared_detector_count = len(declared_detectors)
def _declared_with_status(status: str) -> list[str]:
names: list[str] = []
declared = set(declared_detectors)
if not declared:
return names
for item in results:
if not isinstance(item, dict) or str(item.get("status") or "") != status:
continue
detector_name = str(item.get("detector") or "").strip()
if detector_name in declared and detector_name not in names:
names.append(detector_name)
return names[:limit]
return { return {
"status": status, "status": status,
"detector_count": int(summary.get("detector_count") or len(results)), "detector_count": int(summary.get("detector_count") or len(results)),
@ -6382,7 +6237,6 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
"failed_detectors": _detectors_with_status("fail"), "failed_detectors": _detectors_with_status("fail"),
"review_detectors": _detectors_with_status("review"), "review_detectors": _detectors_with_status("review"),
"skipped_detectors": _detectors_with_status("skipped"), "skipped_detectors": _detectors_with_status("skipped"),
"declared_skipped_detectors": _declared_with_status("skipped"),
"signal_ok_for_auto_coder": status in {"fail", "review"}, "signal_ok_for_auto_coder": status in {"fail", "review"},
} }
@ -6393,9 +6247,6 @@ def evaluate_detector_results_gate(detector_results_summary: dict[str, Any]) ->
detector_count = int(detector_results_summary.get("detector_count") or 0) detector_count = int(detector_results_summary.get("detector_count") or 0)
if declared_detector_count > 0 and detector_count == 0: if declared_detector_count > 0 and detector_count == 0:
return False, "declared_detector_results_missing" return False, "declared_detector_results_missing"
declared_skipped = normalize_string_list(detector_results_summary.get("declared_skipped_detectors"))
if declared_skipped:
return False, f"declared_detector_results_skipped:{','.join(declared_skipped)}"
if declared_detector_count > 0 and status == "skipped": if declared_detector_count > 0 and status == "skipped":
return False, "declared_detector_results_skipped" return False, "declared_detector_results_skipped"
if status == "fail": if status == "fail":

View File

@ -720,11 +720,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--prompt-fingerprint", help="Optional prompt fingerprint metadata.") parser.add_argument("--prompt-fingerprint", help="Optional prompt fingerprint metadata.")
parser.add_argument( parser.add_argument(
"--validated-run-dir", "--validated-run-dir",
help=( help="Accepted truth-harness artifact directory containing pack_state.json, truth_review.json, and business_review.json.",
"Accepted replay artifact directory. Supports truth-harness runs "
"(pack_state/truth_review/business_review), domain-case-loop pack/scenario dirs, "
"and run-pack-loop dirs with loop_state.json."
),
) )
parser.add_argument( parser.add_argument(
"--allow-unvalidated", "--allow-unvalidated",

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import sys import sys
import tempfile import tempfile
import unittest import unittest
@ -23,10 +22,6 @@ def write_text(path: Path, text: str) -> None:
path.write_text(text, encoding="utf-8") path.write_text(text, encoding="utf-8")
def to_win1251_mojibake(value: str) -> str:
return value.encode("utf-8").decode("cp1251")
class AgentDetectorRunnerTests(unittest.TestCase): class AgentDetectorRunnerTests(unittest.TestCase):
def test_default_runner_fails_missing_effective_runtime(self) -> None: def test_default_runner_fails_missing_effective_runtime(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
@ -107,59 +102,6 @@ class AgentDetectorRunnerTests(unittest.TestCase):
self.assertEqual(results["summary"]["status"], "pass") self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["status"], "pass") self.assertEqual(results["results"][0]["status"], "pass")
def test_limited_next_action_accepts_mojibake_what_to_check_next(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "run"
answer = (
"Это не подтвержденная чистая прибыль.\n"
"Что проверить дальше: чистую прибыль через счета 90/91/99 и закрытие периода."
)
write_text(artifact_dir / "steps" / "s01" / "output.md", to_win1251_mojibake(answer))
registry_path = root / "detector_registry.json"
issue_catalog_path = root / "issue_catalog.json"
write_json(
registry_path,
{
"schema_version": "agent_detector_registry_v1",
"detectors": {
"limited_answer_without_next_action": {
"kind": "answer_text_required_when_limited",
"automation_level": "semi_automatic",
"description": "Limited answers need a next action.",
"issue_codes": ["business_next_step_missing"],
"inputs": ["output.md"],
"check": {
"limited_patterns": ["(?i)не подтвержден"],
"required_next_action_patterns_any": ["(?i)(можно|проверь)"],
},
}
},
},
)
write_json(
issue_catalog_path,
{
"schema_version": "agent_issue_catalog_v1",
"issues": {
"business_next_step_missing": {
"detectors": ["limited_answer_without_next_action"],
}
},
},
)
results = runner.build_detector_results(
artifact_dir,
issue_codes=["business_next_step_missing"],
registry_path=registry_path,
issue_catalog_path=issue_catalog_path,
include_default_global=False,
)
self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["message"], "limited answer includes a next action")
def test_candidate_forbidden_regex_is_limited_to_evidence_path(self) -> None: def test_candidate_forbidden_regex_is_limited_to_evidence_path(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)
@ -229,7 +171,7 @@ class AgentDetectorRunnerTests(unittest.TestCase):
root = Path(tmp) root = Path(tmp)
artifact_dir = root / "run" artifact_dir = root / "run"
write_text( write_text(
artifact_dir / "scenarios" / "profit_cashflow_boundary" / "steps" / "step_01_margin_root" / "output.md", artifact_dir / "scenarios" / "margin_root" / "steps" / "step_01" / "output.md",
"Top margin item: revenue 100, cogs 60, gross profit 40, margin 40%.", "Top margin item: revenue 100, cogs 60, gross profit 40, margin 40%.",
) )
write_text( write_text(
@ -241,15 +183,6 @@ class AgentDetectorRunnerTests(unittest.TestCase):
/ "output.md", / "output.md",
"Supplier for the selected item: Example LLC.", "Supplier for the selected item: Example LLC.",
) )
write_text(
artifact_dir
/ "scenarios"
/ "business_health"
/ "steps"
/ "step_05_next_checks"
/ "output.md",
"Check profit, debt, inventory, taxes, and customer concentration next.",
)
registry_path = root / "detector_registry.json" registry_path = root / "detector_registry.json"
issue_catalog_path = root / "issue_catalog.json" issue_catalog_path = root / "issue_catalog.json"
write_json( write_json(
@ -265,7 +198,6 @@ class AgentDetectorRunnerTests(unittest.TestCase):
"inputs": ["output.md"], "inputs": ["output.md"],
"check": { "check": {
"required_patterns_any": ["(?i)(revenue|cogs|gross profit|margin)"], "required_patterns_any": ["(?i)(revenue|cogs|gross profit|margin)"],
"artifact_path_include_patterns": ["(?i)steps[\\\\/][^\\\\/]*(?:margin|profitability)"],
"artifact_path_exclude_patterns": ["(?i)selected_item"], "artifact_path_exclude_patterns": ["(?i)selected_item"],
}, },
} }
@ -286,183 +218,7 @@ class AgentDetectorRunnerTests(unittest.TestCase):
self.assertEqual(results["results"][0]["status"], "pass") self.assertEqual(results["results"][0]["status"], "pass")
evidence_paths = [item["path"] for item in results["results"][0]["evidence"]] evidence_paths = [item["path"] for item in results["results"][0]["evidence"]]
self.assertEqual(len(evidence_paths), 1) self.assertEqual(len(evidence_paths), 1)
self.assertIn("step_01_margin_root", evidence_paths[0]) self.assertIn("margin_root", evidence_paths[0])
def test_counterparty_value_flow_required_surface_scopes_to_current_net_step(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "run"
write_text(
artifact_dir
/ "scenarios"
/ "mixed_planner_counterparty_evidence_and_living_guard"
/ "steps"
/ "step_03_incoming_by_resolved_entity"
/ "output.md",
"Входящие денежные поступления по контрагенту Группа СВК за 2020: 12 093 465 руб.",
)
write_text(
artifact_dir
/ "scenarios"
/ "mixed_planner_counterparty_evidence_and_living_guard"
/ "steps"
/ "step_05_net_after_payout"
/ "output.md",
"По контрагенту Группа СВК за период 2020 получили 12 093 465 руб., заплатили 0 руб.; расчетное нетто в нашу сторону: 12 093 465 руб.",
)
registry_path = root / "detector_registry.json"
issue_catalog_path = root / "issue_catalog.json"
write_json(
registry_path,
{
"schema_version": "agent_detector_registry_v1",
"detectors": {
"counterparty_value_flow_required_surface": {
"kind": "answer_text_required_any",
"automation_level": "automatic",
"description": "Net value-flow answer must surface all money directions.",
"issue_codes": ["counterparty_value_flow_misrouted_to_company_profit"],
"inputs": ["output.md"],
"check": {
"artifact_path_include_patterns": ["(?i)step_05_net_after_payout"],
"required_patterns_any": [
"(?is)(?=.*(СВК|Группа\\s+СВК))(?=.*(входящ|получил|получено|получили))(?=.*(исходящ|заплатил|заплачено|заплатили|ушло))(?=.*(нетто|сальдо|разниц|чистый\\s+денежный))"
],
},
}
},
},
)
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
results = runner.build_detector_results(
artifact_dir,
detector_names=["counterparty_value_flow_required_surface"],
registry_path=registry_path,
issue_catalog_path=issue_catalog_path,
include_default_global=False,
)
self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["status"], "pass")
evidence_paths = [item["path"] for item in results["results"][0]["evidence"]]
self.assertEqual(len(evidence_paths), 1)
self.assertIn("step_05_net_after_payout", evidence_paths[0])
def test_answer_text_shape_uses_business_first_step_review(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "run"
step_dir = artifact_dir / "scenarios" / "mixed" / "steps" / "step_01"
write_text(step_dir / "output.md", "Direct business answer first.")
write_json(step_dir / "step_state.json", {"business_first_review": {"direct_answer_first_ok": True}})
registry_path = root / "detector_registry.json"
issue_catalog_path = root / "issue_catalog.json"
write_json(
registry_path,
{
"schema_version": "agent_detector_registry_v1",
"detectors": {
"first_line_not_direct_answer": {
"kind": "answer_text_shape",
"automation_level": "semi_automatic",
"description": "First line should be direct.",
"issue_codes": ["business_direct_answer_missing"],
"inputs": ["output.md"],
"check": {"first_line_should_be": "business_answer_or_honest_boundary"},
}
},
},
)
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
results = runner.build_detector_results(
artifact_dir,
detector_names=["first_line_not_direct_answer"],
registry_path=registry_path,
issue_catalog_path=issue_catalog_path,
include_default_global=False,
)
self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["status"], "pass")
def test_answer_text_shape_fails_when_business_first_review_rejects_first_line(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "run"
step_dir = artifact_dir / "scenarios" / "mixed" / "steps" / "step_01"
write_text(step_dir / "output.md", "Let me inspect the route first.")
write_json(step_dir / "step_state.json", {"business_first_review": {"direct_answer_first_ok": False}})
registry_path = root / "detector_registry.json"
issue_catalog_path = root / "issue_catalog.json"
write_json(
registry_path,
{
"schema_version": "agent_detector_registry_v1",
"detectors": {
"first_line_not_direct_answer": {
"kind": "answer_text_shape",
"automation_level": "semi_automatic",
"description": "First line should be direct.",
"issue_codes": ["business_direct_answer_missing"],
"inputs": ["output.md"],
"check": {"first_line_should_be": "business_answer_or_honest_boundary"},
}
},
},
)
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
results = runner.build_detector_results(
artifact_dir,
detector_names=["first_line_not_direct_answer"],
registry_path=registry_path,
issue_catalog_path=issue_catalog_path,
include_default_global=False,
)
self.assertEqual(results["summary"]["status"], "fail")
self.assertEqual(results["results"][0]["status"], "fail")
def test_step_state_lookup_handles_long_artifact_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
step_dir = (
root
/ "run"
/ "scenarios"
/ ("mixed_planner_counterparty_evidence_and_living_guard_" + "s" * 48)
/ "steps"
/ ("step_04_payout_switch_by_resolved_entity_" + "x" * 80)
)
output_path = step_dir / "output.md"
step_state_path = step_dir / "step_state.json"
try:
os.makedirs(runner.path_for_io(step_dir), exist_ok=True)
with open(runner.path_for_io(output_path), "w", encoding="utf-8") as handle:
handle.write("Direct business answer first.")
runner.write_json(step_state_path, {"business_first_review": {"direct_answer_first_ok": True}})
self.assertGreater(len(str(step_state_path.absolute())), 260)
self.assertEqual(runner.output_step_state_path(output_path), step_state_path)
self.assertEqual(
runner.read_json_object(step_state_path).get("business_first_review"),
{"direct_answer_first_ok": True},
)
finally:
for path in (output_path, step_state_path):
try:
os.remove(runner.path_for_io(path))
except OSError:
pass
current = step_dir
while current != root:
try:
os.rmdir(runner.path_for_io(current))
except OSError:
pass
current = current.parent
def test_composite_detector_fails_after_child_detector_fails(self) -> None: def test_composite_detector_fails_after_child_detector_fails(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:

View File

@ -404,24 +404,6 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
self.assertEqual(summary["declared_detector_count"], 1) self.assertEqual(summary["declared_detector_count"], 1)
self.assertEqual(reason, "declared_detector_results_missing") self.assertEqual(reason, "declared_detector_results_missing")
def test_detector_gate_blocks_declared_detector_skipped_inside_mixed_pass_run(self) -> None:
summary = dcl.summarize_detector_results(
{
"summary": {"status": "pass", "detector_count": 2, "pass": 1, "fail": 0, "review": 0, "skipped": 1},
"results": [
{"detector": "runtime_tokens_in_user_answer", "status": "pass"},
{"detector": "counterparty_value_flow_profit_mismatch", "status": "skipped"},
],
"declared_detectors_under_test": ["counterparty_value_flow_profit_mismatch"],
}
)
ok, reason = dcl.evaluate_detector_results_gate(summary)
self.assertFalse(ok)
self.assertEqual(summary["declared_skipped_detectors"], ["counterparty_value_flow_profit_mismatch"])
self.assertEqual(reason, "declared_detector_results_skipped:counterparty_value_flow_profit_mismatch")
def test_auto_coder_gate_allows_when_detector_results_confirm_failure(self) -> None: def test_auto_coder_gate_allows_when_detector_results_confirm_failure(self) -> None:
repair_targets = { repair_targets = {
"targets": [ "targets": [

View File

@ -153,81 +153,6 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertEqual(dcl.derive_scenario_execution_status(step_outputs), "partial") self.assertEqual(dcl.derive_scenario_execution_status(step_outputs), "partial")
self.assertEqual(dcl.derive_scenario_status(step_outputs), "accepted") self.assertEqual(dcl.derive_scenario_status(step_outputs), "accepted")
def test_clean_meta_smalltalk_is_exact_without_1c_capability(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="meta_chat_demo",
domain="agentic_loop",
step={
"step_id": "step_01",
"title": "Human smalltalk",
"depends_on": [],
"question_template": "привет, ты на связи?",
"semantic_tags": ["human_answer", "meta_smalltalk", "mcp_discovery_gate_sanity"],
"required_answer_shape": "direct_answer_first",
},
step_index=1,
question_resolved="привет, ты на связи?",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual_with_explanation",
"text": "Привет! Да, я на связи. Готов помочь с анализом данных из 1С в режиме чтения.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "chat",
"fallback_type": "none",
"living_chat_response_source": "llm_chat",
},
"session_summary": {},
},
entries=[],
)
self.assertTrue(step_state["clean_meta_chat_answer_validated"])
self.assertEqual(step_state["execution_status"], "exact")
self.assertEqual(step_state["acceptance_status"], "validated")
def test_off_domain_living_chat_is_exact_without_1c_capability(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="off_domain_demo",
domain="agentic_loop",
step={
"step_id": "step_09",
"title": "Off-domain living chat",
"depends_on": [],
"question_template": "а чем капибара отличается от утки?",
"semantic_tags": ["off_domain_living_chat", "stale_replay_forbidden", "context_boundary"],
"required_answer_shape": "direct_answer_first",
},
step_index=9,
question_resolved="а чем капибара отличается от утки?",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual_with_explanation",
"text": (
"Капибара и утка отличаются принципиально: капибара - млекопитающее-грызун, "
"а утка - птица. Поэтому у них разные тело, среда обитания и способ передвижения."
),
"message_id": "msg-2",
"trace_id": "trace-2",
},
"technical_debug_payload": {
"detected_mode": "chat",
"fallback_type": "none",
"living_chat_response_source": "llm_chat",
},
"session_summary": {},
},
entries=[],
)
self.assertTrue(step_state["clean_meta_chat_answer_validated"])
self.assertEqual(step_state["execution_status"], "exact")
self.assertEqual(step_state["acceptance_status"], "validated")
def test_today_scope_required_filter_and_direct_patterns_are_enforced(self) -> None: def test_today_scope_required_filter_and_direct_patterns_are_enforced(self) -> None:
self.assertTrue(dcl.question_resets_temporal_scope("мы должны комуто денег на сегодня?")) self.assertTrue(dcl.question_resets_temporal_scope("мы должны комуто денег на сегодня?"))
@ -265,109 +190,6 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertIn("required_answer_patterns_any_missing", step_state["violated_invariants"]) self.assertIn("required_answer_patterns_any_missing", step_state["violated_invariants"])
self.assertEqual(step_state["acceptance_status"], "rejected") self.assertEqual(step_state["acceptance_status"], "rejected")
def test_binding_placeholder_required_filter_is_resolved_before_step_validation(self) -> None:
step = dcl.normalize_step_definition(
1,
{
"step_id": "counterparty_binding_filter",
"title": "Counterparty binding filter",
"question": "Отдельно по {{bindings.counterparty}}: какой это поток?",
"required_filters": {"counterparty": "{{bindings.counterparty}}"},
"required_answer_patterns_all": ["(?i){{bindings.counterparty}}|банк"],
},
)
scenario_state = {
"bindings": {"counterparty": "СБЕРБАНК"},
"step_outputs": {},
"semantic_memory": {},
}
resolved_step = dcl.resolve_step_contract_placeholders(step, scenario_state)
self.assertEqual(resolved_step["required_filters"]["counterparty"], "СБЕРБАНК")
self.assertEqual(resolved_step["required_answer_patterns_all"], ["(?i)СБЕРБАНК|банк"])
step_state = dcl.build_scenario_step_state(
scenario_id="binding_filter_demo",
domain="agentic_loop",
step=resolved_step,
step_index=1,
question_resolved="Отдельно по СБЕРБАНК: какой это поток?",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual",
"text": "СБЕРБАНК подтвержден как банковский/финансовый поток.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "address_query",
"detected_intent": "bank_operations_by_counterparty",
"selected_recipe": "address_bank_operations_by_counterparty_v1",
"capability_id": "bank_operations_drilldown",
"truth_gate_contract_status": "full_confirmed",
"mcp_call_status": "matched_non_empty",
"extracted_filters": {"counterparty": "СБЕРБАНК"},
},
"session_summary": {},
},
entries=[],
)
self.assertNotIn("missing_required_filter", step_state["violated_invariants"])
def test_expected_intent_accepts_discovery_chain_id(self) -> None:
self.assertTrue(
dcl.step_intent_matches_expected(
{
"detected_intent": "supplier_payouts_profile",
"mcp_discovery_selected_chain_id": "value_flow",
"mcp_discovery_route_candidate_fact_family": "value_flow",
"mcp_discovery_route_candidate_action_family": "payout",
},
["value_flow"],
)
)
def test_bare_organization_scope_selection_is_validated_memory_checkpoint(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="org_scope_demo",
domain="agentic_loop",
step={
"step_id": "step_01",
"title": "Choose organization",
"depends_on": [],
"question_template": "ООО Альтернатива Плюс",
"semantic_tags": ["bare_org_scope", "company_scope"],
"required_answer_patterns_all": ["(?i)фиксир|рабоч.*организац|контур"],
},
step_index=1,
question_resolved="ООО Альтернатива Плюс",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual_with_explanation",
"text": 'Отлично, фиксирую рабочую организацию: ООО "Альтернатива Плюс".',
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "chat",
"fallback_type": "none",
"living_chat_response_source": "deterministic_data_scope_selection_contract",
"living_chat_data_scope_probe_status": "resolved",
"living_chat_selected_organization": 'ООО "Альтернатива Плюс"',
"assistant_active_organization": 'ООО "Альтернатива Плюс"',
},
"session_summary": {},
},
entries=[],
)
self.assertEqual(step_state["status"], "validated")
self.assertTrue(step_state["memory_checkpoint_validated"])
def test_repair_targets_promote_route_candidate_enablement_gaps(self) -> None: def test_repair_targets_promote_route_candidate_enablement_gaps(self) -> None:
repair_targets = dcl.build_deterministic_repair_targets( repair_targets = dcl.build_deterministic_repair_targets(
{"pack_id": "route_candidate_pack", "domain": "open_world", "final_status": "accepted"}, {"pack_id": "route_candidate_pack", "domain": "open_world", "final_status": "accepted"},
@ -685,32 +507,6 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertIsNone(step_state["date_scope"]["as_of_date"]) self.assertIsNone(step_state["date_scope"]["as_of_date"])
self.assertEqual(step_state["date_scope"]["source"], "question_temporal_scope_reset") self.assertEqual(step_state["date_scope"]["source"], "question_temporal_scope_reset")
def test_out_of_window_document_dates_reject_validated_step(self) -> None:
validated = dcl.validate_step_contract(
{
"execution_status": "exact",
"required_answer_shape": "direct_answer_first",
"reply_type": "factual",
"actual_direct_answer": "Контрагент: Группа СВК. Найдено документов: 19.",
"assistant_text": (
"Контрагент: Группа СВК. Найдено документов: 19.\n"
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07"
),
"top_non_empty_lines": [
"Контрагент: Группа СВК. Найдено документов: 19.",
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07",
],
"date_scope": {
"period_from": "2020-01-01",
"period_to": "2020-12-31",
},
}
)
self.assertEqual(validated["status"], "rejected")
self.assertIn("out_of_window_date_in_answer", validated["violated_invariants"])
self.assertIn("out_of_window_date_in_answer", validated["warnings"])
def test_open_items_exact_negative_answer_validates_without_rows(self) -> None: def test_open_items_exact_negative_answer_validates_without_rows(self) -> None:
step_state = dcl.build_scenario_step_state( step_state = dcl.build_scenario_step_state(
scenario_id="open_items_negative_demo", scenario_id="open_items_negative_demo",