Стабилизировать маржинальность номенклатуры 1С

This commit is contained in:
2026-05-23 14:39:37 +03:00
parent 473cdc3a9b
commit a15f24f21d
14 changed files with 760 additions and 18 deletions
@@ -81,6 +81,13 @@ function inventoryProfitabilityPeriodLabel(options, deps) {
const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null;
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
}
function asksForInventoryCostBaseRows(userMessage) {
const text = String(userMessage ?? "").toLowerCase();
if (!/(?:покажи|показать|выведи|вывести|дай|дать|раскрой|раскрыть|строк|строки|строку|баз)/iu.test(text)) {
return false;
}
return /(?:себестоимостн|себестоимост|себестоим|закупочн|закупк|90\.02|\b41\b|баз)/iu.test(text);
}
function inventoryRowItemLabel(row, deps) {
return deps.summarizeInventoryTraceRows([row]).item;
}
@@ -459,7 +466,12 @@ function composeInventoryReply(intent, rows, options, deps) {
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
const totalSpread = totalRevenue - totalCostProxy;
if (confirmedEntries.length === 0) {
const lines = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`];
const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage);
const lines = [
costBaseRowsRequested && purchasesWithoutSales.length === 0
? `За период ${periodLabel} подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено.`
: `За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`
];
const findings = [];
if (salesWithoutCost.length > 0) {
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
@@ -470,6 +482,9 @@ function composeInventoryReply(intent, rows, options, deps) {
: "Подтвержденной себестоимости реализации по этим позициям не найдено.");
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
}
if (costBaseRowsRequested && purchasesWithoutSales.length === 0) {
findings.push("Строк себестоимости реализации / себестоимостной базы для показа нет.");
}
if (purchasesWithoutSales.length > 0) {
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
const purchaseItemPhrase = purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`;
@@ -493,7 +508,7 @@ function composeInventoryReply(intent, rows, options, deps) {
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что можно сделать дальше:", nextActions);
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", [
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
"Это не чистая прибыль компании и не замена закрытию месяца."
"Это не показатель чистой прибыли и не замена закрытию месяца."
]);
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(entries.length > 0 ? "medium" : "weak", false));
}
@@ -508,7 +523,7 @@ function composeInventoryReply(intent, rows, options, deps) {
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Низкая или отрицательная валовая маржинальность:", lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
}
const boundaryLines = [
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.",
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не показатель чистой прибыли.",
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
];
if (salesWithoutCost.length > 0) {
@@ -350,6 +350,24 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
routeMode === "exact" ||
hasFullConfirmedTruth(input));
}
function hasInventoryMarginRankingAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!toNonEmptyString(input.currentReply)) {
return false;
}
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
return false;
}
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);
return Boolean(detectedIntent === "inventory_margin_ranking_for_nomenclature" ||
selectedRecipe === "address_inventory_margin_ranking_for_nomenclature_v1" ||
capabilityId === "inventory_inventory_margin_ranking_for_nomenclature");
}
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
@@ -621,6 +639,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
@@ -685,6 +704,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (exactBankOperationsAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
}
if (inventoryMarginRankingAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply");
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
}
@@ -715,6 +737,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
!exactBankOperationsAddressReply &&
!inventoryMarginRankingAddressReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
@@ -315,6 +315,21 @@ function createAssistantRoutePolicy(deps) {
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return hasRequestCue && hasTemporalCue;
}
function hasInventoryMarginRankingContinuationSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
if (!normalized) {
return false;
}
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list)/iu.test(normalized) &&
/(?:найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
/(?:себестоимостн|реализац|марж|прибыл|номенклатур)/iu.test(normalized);
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
/(?:\bне\b|вместо|а\s+не|not|instead)/iu.test(normalized);
const periodExpansion = /(?:расширь|расширить|возьми|давай|покажи|за|на|до|весь|год|квартал|месяц|expand|period)/iu.test(normalized) &&
/(?:январ|феврал|март|апрел|ма[йяе]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return wantsFoundRows || account41Not01 || periodExpansion;
}
function hasOrganizationClarificationTextCue(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
@@ -481,6 +496,7 @@ function createAssistantRoutePolicy(deps) {
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
const followupRootIntent = toNonEmptyString(followupContext?.root_intent);
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
? followupContext.previous_filters
: null;
@@ -515,6 +531,16 @@ function createAssistantRoutePolicy(deps) {
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
const protectedInventoryMarginRankingFollowup = Boolean(followupContext &&
(followupPreviousIntent === "inventory_margin_ranking_for_nomenclature" ||
followupRootIntent === "inventory_margin_ranking_for_nomenclature") &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dangerOrCoercionSignal &&
(hasInventoryMarginRankingContinuationSignal(rawUserMessage) ||
hasInventoryMarginRankingContinuationSignal(repairedRawUserMessage) ||
hasInventoryMarginRankingContinuationSignal(effectiveAddressUserMessage) ||
hasInventoryMarginRankingContinuationSignal(repairedEffectiveAddressUserMessage)));
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
lastOrganizationClarificationDebug &&
explicitOrganizationClarificationSelection &&
@@ -571,6 +597,7 @@ function createAssistantRoutePolicy(deps) {
!baseToolGatePreservesAddressLane &&
!effectiveGroundedValueFlowFollowupContextDetected &&
!protectedInventoryShortFollowup &&
!protectedInventoryMarginRankingFollowup &&
!organizationClarificationContinuationDetected &&
!routeCandidateOrganizationClarificationDetected);
const lastAddressAssistantDebug = sessionItems
@@ -1080,6 +1107,7 @@ function createAssistantRoutePolicy(deps) {
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
protectedInventoryMarginRankingFollowup ||
inventoryRootRestatementFollowupDetected);
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
@@ -1116,7 +1144,9 @@ function createAssistantRoutePolicy(deps) {
resolvedIntentResolution.intent === "unknown" &&
(!llmContractIntent || llmContractIntent === "unknown"));
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal);
const protectAddressLaneFromFallback = Boolean(laneProtectionArbitration.protectAddressLaneFromFallback ||
customerValueRankingAddressSignal ||
protectedInventoryMarginRankingFollowup);
const vatExplainFollowupSignal = Boolean(followupContext &&
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
@@ -1196,7 +1226,7 @@ function createAssistantRoutePolicy(deps) {
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
supportedAddressRouteCandidateDetected &&
(supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) &&
!deepAnalysisPreferenceDetected &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep &&
@@ -1205,9 +1235,11 @@ function createAssistantRoutePolicy(deps) {
if (semanticAddressLaneRecovery) {
runAddressLane = true;
toolGateDecision = "run_address_lane";
toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent
? "address_intent_resolver_detected"
: "address_signal_detected";
toolGateReason = protectedInventoryMarginRankingFollowup
? "followup_context_detected"
: resolvedIntentResolution.intent !== "unknown" || llmContractIntent
? "address_intent_resolver_detected"
: "address_signal_detected";
}
if (unsupportedAddressIntentFallbackToDeep) {
runAddressLane = false;