Закрыть phase96 reviewed-route складских резервов и ликвидности

This commit is contained in:
2026-05-13 00:11:01 +03:00
parent b99a3be083
commit 5220cb2e0e
24 changed files with 1189 additions and 139 deletions
+145 -35
View File
@@ -283,6 +283,61 @@ const INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE = `
УПОРЯДОЧИТЬ ПО
Количество __ORDER_DIRECTION__
`;
const INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Списание.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Списание.Ссылка) КАК Регистратор,
"Списание товаров" КАК ТипСобытия,
ПРЕДСТАВЛЕНИЕ(Списание.Организация) КАК Организация,
ПРЕДСТАВЛЕНИЕ(Списание.Склад) КАК Склад,
Списание.СуммаДокумента КАК Сумма,
Списание.Основание КАК Основание,
Списание.Комментарий КАК Комментарий
ИЗ
Документ.СписаниеТоваров КАК Списание
__WHERE_WRITE_OFF__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Оприходование.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Оприходование.Ссылка) КАК Регистратор,
"Оприходование товаров" КАК ТипСобытия,
ПРЕДСТАВЛЕНИЕ(Оприходование.Организация) КАК Организация,
ПРЕДСТАВЛЕНИЕ(Оприходование.Склад) КАК Склад,
Оприходование.СуммаДокумента КАК Сумма,
Оприходование.Основание КАК Основание,
Оприходование.Комментарий КАК Комментарий
ИЗ
Документ.ОприходованиеТоваров КАК Оприходование
__WHERE_RECEIPT__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Инвентаризация.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Ссылка) КАК Регистратор,
"Инвентаризация товаров на складе" КАК ТипСобытия,
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Организация) КАК Организация,
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Склад) КАК Склад,
0 КАК Сумма,
Инвентаризация.ПричинаПроведенияИнвентаризации КАК Основание,
Инвентаризация.Комментарий КАК Комментарий
ИЗ
Документ.ИнвентаризацияТоваровНаСкладе КАК Инвентаризация
__WHERE_INVENTORY_COUNT__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Переоценка.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Переоценка.Ссылка) КАК Регистратор,
"Переоценка товаров в рознице" КАК ТипСобытия,
ПРЕДСТАВЛЕНИЕ(Переоценка.Организация) КАК Организация,
ПРЕДСТАВЛЕНИЕ(Переоценка.Склад) КАК Склад,
0 КАК Сумма,
"" КАК Основание,
Переоценка.Комментарий КАК Комментарий
ИЗ
Документ.ПереоценкаТоваровВРознице КАК Переоценка
__WHERE_REVALUATION__
УПОРЯДОЧИТЬ ПО
Период __ORDER_DIRECTION__
`;
const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период,
@@ -933,6 +988,16 @@ const BASE_RECIPES = [
account_scope_mode: "strict",
query_template: "inventory_aging_by_purchase_date_profile"
},
{
recipe_id: "address_inventory_quality_events_for_organization_v1",
intent: "inventory_quality_events_for_organization",
purpose: "Check posted inventory quality event documents: write-offs, stocktaking, receipt adjustments, and retail revaluation",
required_filters: [],
optional_filters: ["as_of_date", "period_from", "period_to", "organization", "warehouse", "limit", "sort"],
default_limit: 400,
account_scope_mode: "preferred",
query_template: "inventory_quality_events_profile"
},
{
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
intent: "open_contracts_confirmed_as_of_date",
@@ -1326,6 +1391,48 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildWarehouseReferenceCondition(filters, fieldPaths) {
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
if (!warehouse) {
return null;
}
const tokens = Array.from(new Set(warehouse
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
.map((token) => token.trim())
.filter((token) => token.length >= 3)
.filter((token) => !["склад", "warehouse"].includes(token.toLowerCase()))));
const effectiveTokens = tokens.length > 0 ? tokens : [warehouse];
const clauses = fieldPaths
.map((fieldPath) => String(fieldPath ?? "").trim())
.filter((fieldPath) => fieldPath.length > 0)
.map((fieldPath) => {
const tokenConditions = effectiveTokens.map((token) => {
const escapedToken = toQueryStringLiteral(token);
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
});
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
});
if (clauses.length === 0) {
return null;
}
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
}
function buildInventoryQualityDocumentWhereClause(filters, dateFieldPath, organizationFieldPath, warehouseFieldPath) {
return buildWhereClause(filters, dateFieldPath, [
`${dateFieldPath.replace(/\.Дата$/u, ".Проведен")} = ИСТИНА`,
buildOrganizationReferenceCondition(filters, [organizationFieldPath]),
buildWarehouseReferenceCondition(filters, [warehouseFieldPath])
].filter((item) => Boolean(item)));
}
function buildInventoryQualityEventsQuery(filters, resolvedLimit) {
return INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_WRITE_OFF__", buildInventoryQualityDocumentWhereClause(filters, "Списание.Дата", "Списание.Организация", "Списание.Склад"))
.replace("__WHERE_RECEIPT__", buildInventoryQualityDocumentWhereClause(filters, "Оприходование.Дата", "Оприходование.Организация", "Оприходование.Склад"))
.replace("__WHERE_INVENTORY_COUNT__", buildInventoryQualityDocumentWhereClause(filters, "Инвентаризация.Дата", "Инвентаризация.Организация", "Инвентаризация.Склад"))
.replace("__WHERE_REVALUATION__", buildInventoryQualityDocumentWhereClause(filters, "Переоценка.Дата", "Переоценка.Организация", "Переоценка.Склад"))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildInventoryItemReferenceCondition(filters, fieldPaths) {
const item = typeof filters.item === "string" ? filters.item.trim() : "";
if (!item) {
@@ -1599,6 +1706,7 @@ function maxLimitForIntent(intent) {
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "inventory_quality_events_for_organization" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "list_contracts_by_counterparty" ||
intent === "list_documents_by_counterparty" ||
@@ -1790,27 +1898,11 @@ function buildAddressRecipePlan(recipe, filters) {
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
: recipe.query_template === "contracts_by_counterparty_profile"
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
: recipe.query_template === "inventory_quality_events_profile"
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
: recipe.query_template === "contracts_by_counterparty_profile"
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
@@ -1825,10 +1917,10 @@ function buildAddressRecipePlan(recipe, filters) {
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
@@ -1843,20 +1935,38 @@ function buildAddressRecipePlan(recipe, filters) {
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
return {
recipe,
query,
@@ -393,6 +393,9 @@ function isVendorRiskBoundaryTurn(pilot) {
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
}
function businessOverviewInventoryUnknownLabel(overview) {
if (overview.inventory_quality_events) {
return "рыночная ликвидационная стоимость и управленческий резерв склада";
}
if (overview.inventory_staleness_risk_proxy) {
return "резервы/списания/ликвидационная стоимость склада";
}
@@ -621,6 +624,24 @@ function businessOverviewVendorProcurementQualityText(overview) {
}
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
}
function businessOverviewInventoryQualityEventsText(overview) {
const quality = overview.inventory_quality_events;
if (!quality) {
return null;
}
const period = quality.period_scope ?? "проверенное окно";
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
const eventWindow = quality.first_event_date && quality.latest_event_date
? ` Окно найденных событий: ${quality.first_event_date} - ${quality.latest_event_date}.`
: "";
if (quality.evidence_status === "reviewed_no_quality_events_found") {
return `Коротко: проверил складские документы списания, оприходования, инвентаризации и переоценки${organization} за ${period}; подтвержденных событий списания/корректировки/инвентаризации/переоценки не найдено. Это сильный отрицательный сигнал по доступным документам 1С, но не рыночная ликвидационная стоимость и не управленческий резерв под неликвиды.`;
}
if (quality.evidence_status === "reviewed_inventory_control_events_only") {
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний и оприходований/корректировок с суммой не найдено, но есть инвентаризации ${quality.inventory_count_rows} и переоценки ${quality.revaluation_rows}.${eventWindow} Это контрольные складские документы, а не подтвержденный резерв или рыночная ликвидационная оценка.`;
}
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С по складским событиям, но не самостоятельная рыночная ликвидационная стоимость и не расчет управленческого резерва.`;
}
function headlineFor(mode, pilot) {
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
@@ -653,6 +674,10 @@ function headlineFor(mode, pilot) {
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
}
if (isInventoryReserveBoundaryTurn(pilot)) {
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
if (inventoryQualityEventsText) {
return inventoryQualityEventsText;
}
const inventoryBasis = overview.inventory_staleness_risk_proxy
? "есть только складской staleness-risk proxy по найденным строкам"
: overview.inventory_position || overview.inventory_turnover_proxy
@@ -724,6 +749,9 @@ function headlineFor(mode, pilot) {
if (overview.inventory_staleness_risk_proxy) {
families.push("staleness risk proxy склада");
}
if (overview.inventory_quality_events) {
families.push("складские quality-события");
}
const unknownFamilies = overview.accounting_financial_result
? ["аудированная/юридически подтвержденная прибыль"]
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
@@ -946,6 +974,9 @@ function buildMustNotClaim(pilot) {
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
if (pilot.derived_business_overview?.inventory_quality_events) {
claims.push("Do not present reviewed inventory quality events as confirmed obsolete stock, reserve policy, market liquidation value, management reserve, or full inventory health.");
}
if (pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)) {
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
@@ -1436,6 +1467,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
const proxy = overview.inventory_staleness_risk_proxy;
lines.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`);
}
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
if (inventoryQualityEventsText) {
lines.push(inventoryQualityEventsText.replace(/^Коротко:\s*/u, ""));
}
return lines;
}
function businessOverviewCashSynthesisLine(overview) {
@@ -1603,6 +1638,15 @@ function businessOverviewRiskSynthesisLine(overview) {
if (overview.inventory_staleness_risk_proxy) {
signals.push(`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.`);
}
if (overview.inventory_quality_events) {
const quality = overview.inventory_quality_events;
if (quality.evidence_status === "reviewed_no_quality_events_found") {
signals.push("складские quality-события: документы списания, оприходования, инвентаризации и переоценки проверены, подтвержденных событий не найдено");
}
else {
signals.push(`складские quality-события: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}`);
}
}
return signals.length > 0
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
: null;
@@ -1616,7 +1660,8 @@ function businessOverviewExecutiveVerdictLine(overview) {
overview.debt_staleness_risk_proxy ||
overview.inventory_position ||
overview.inventory_turnover_proxy ||
overview.inventory_staleness_risk_proxy);
overview.inventory_staleness_risk_proxy ||
overview.inventory_quality_events);
const hasOperationalProfileSignal = Boolean(overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile);
const hasExtraSignals = hasTaxDebtInventorySignals || hasOperationalProfileSignal;
if (!hasCash && !hasExtraSignals) {
@@ -1750,6 +1795,10 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy");
}
if (pilot.derived_business_overview?.inventory_quality_events) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_quality_events");
pushReason(reasonCodes, `answer_contains_business_overview_inventory_quality_events_${pilot.derived_business_overview.inventory_quality_events.evidence_status}`);
}
if (pilot.derived_business_overview?.missing_proof_families?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_missing_proof_ledger");
}
@@ -211,6 +211,16 @@ function shouldRunDebtDueDateAgingProbe(planner) {
.join(" ");
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
}
function shouldRunInventoryQualityEventsProbe(planner) {
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
.filter((item) => Boolean(item))
.join(" ");
return /(?:inventory_reserve|reserve_liquidation|liquidation|write[-_ ]?off|obsolete|obsolescence|inventory_reserve_liquidation_quality|резерв|списан|ликвидац|неликвид|обесцен)/iu.test(combined);
}
function buildBusinessOverviewInventoryFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
@@ -3021,6 +3031,69 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input) {
inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows"
};
}
function rowInventoryQualityEventType(row) {
return rowTextValue(row, ["ТипСобытия", "EventType", "event_type", "Регистратор", "Registrator", "registrator"]) ?? "";
}
function deriveBusinessOverviewInventoryQualityEvents(input) {
const result = input.inventoryQualityEventsResult;
if (!result || result.error) {
return null;
}
let writeoffRows = 0;
let writeoffAmount = 0;
let receiptAdjustmentRows = 0;
let receiptAdjustmentAmount = 0;
let inventoryCountRows = 0;
let revaluationRows = 0;
const eventDates = [];
for (const row of result.rows) {
const eventType = rowInventoryQualityEventType(row);
const amount = rowAmountValue(row) ?? 0;
const date = rowDateValue(row);
if (date) {
eventDates.push(date);
}
if (/списан|write[-_ ]?off/iu.test(eventType)) {
writeoffRows += 1;
writeoffAmount += amount;
continue;
}
if (/оприход|receipt|positive/i.test(eventType)) {
receiptAdjustmentRows += 1;
receiptAdjustmentAmount += amount;
continue;
}
if (/инвентаризац|stocktaking|inventory count/i.test(eventType)) {
inventoryCountRows += 1;
continue;
}
if (/переоцен|revaluation/i.test(eventType)) {
revaluationRows += 1;
}
}
const sortedDates = eventDates.sort((left, right) => left.localeCompare(right));
const evidenceStatus = writeoffRows > 0 || receiptAdjustmentRows > 0
? "reviewed_writeoff_or_adjustment_events_found"
: inventoryCountRows > 0 || revaluationRows > 0
? "reviewed_inventory_control_events_only"
: "reviewed_no_quality_events_found";
return {
period_scope: input.periodScope,
rows_matched: result.matched_rows,
writeoff_rows: writeoffRows,
writeoff_amount: writeoffAmount,
writeoff_amount_human_ru: formatAmountHumanRu(writeoffAmount),
receipt_adjustment_rows: receiptAdjustmentRows,
receipt_adjustment_amount: receiptAdjustmentAmount,
receipt_adjustment_amount_human_ru: formatAmountHumanRu(receiptAdjustmentAmount),
inventory_count_rows: inventoryCountRows,
revaluation_rows: revaluationRows,
first_event_date: sortedDates[0] ?? null,
latest_event_date: sortedDates[sortedDates.length - 1] ?? null,
evidence_status: evidenceStatus,
inference_basis: "inventory_quality_documents_confirmed_1c_rows"
};
}
function deriveBusinessOverviewVendorProcurementQuality(input) {
if (!input.rankedOutgoing ||
input.rankedOutgoing.ranked_values.length <= 0 ||
@@ -3109,10 +3182,10 @@ function buildBusinessOverviewMissingProofFamilies(input) {
must_not_claim: "confirmed_overdue_debt_credit_risk_or_due_date_aging"
});
}
if (missing.has("inventory_position") ||
if ((missing.has("inventory_position") ||
missing.has("inventory_turnover_quality") ||
missing.has("inventory_liquidity_quality") ||
missing.has("inventory_reserve_liquidation_quality")) {
missing.has("inventory_reserve_liquidation_quality")) && !input.inventoryQualityEvents) {
pushUnique({
family: "inventory_reserve_liquidation_quality",
current_status: input.inventoryStalenessRiskProxy
@@ -3195,6 +3268,10 @@ function deriveBusinessOverview(input) {
inventoryPosition,
inventoryTurnoverProxy
});
const inventoryQualityEvents = deriveBusinessOverviewInventoryQualityEvents({
inventoryQualityEventsResult: input.inventoryQualityEventsResult,
periodScope: input.periodScope
});
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
rankedOutgoing,
outgoing,
@@ -3219,6 +3296,7 @@ function deriveBusinessOverview(input) {
Boolean(inventoryPosition),
Boolean(inventoryTurnoverProxy),
Boolean(inventoryStalenessRiskProxy),
Boolean(inventoryQualityEvents),
Boolean(vendorProcurementQuality)
].filter(Boolean).length;
if (checkedSignalCount <= 0) {
@@ -3232,11 +3310,13 @@ function deriveBusinessOverview(input) {
debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
taxPosition ? null : "tax_position",
inventoryPosition
? inventoryStalenessRiskProxy
? "inventory_reserve_liquidation_quality"
: inventoryTurnoverProxy
? "inventory_liquidity_quality"
: "inventory_turnover_quality"
? inventoryQualityEvents
? null
: inventoryStalenessRiskProxy
? "inventory_reserve_liquidation_quality"
: inventoryTurnoverProxy
? "inventory_liquidity_quality"
: "inventory_turnover_quality"
: "inventory_position",
inventoryPosition?.aging_signal ? null : "inventory_aging_quality"
].filter((item) => Boolean(item));
@@ -3250,6 +3330,7 @@ function deriveBusinessOverview(input) {
inventoryPosition,
inventoryTurnoverProxy,
inventoryStalenessRiskProxy,
inventoryQualityEvents,
vendorProcurementQuality,
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
});
@@ -3275,6 +3356,7 @@ function deriveBusinessOverview(input) {
inventory_position: inventoryPosition,
inventory_turnover_proxy: inventoryTurnoverProxy,
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
inventory_quality_events: inventoryQualityEvents,
document_activity_profile: documentActivityProfile,
counterparty_profile: counterpartyProfile,
contract_usage_profile: contractUsageProfile,
@@ -3283,7 +3365,7 @@ function deriveBusinessOverview(input) {
checked_signal_count: checkedSignalCount,
missing_signal_families: missingSignalFamilies,
missing_proof_families: missingProofFamilies,
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || inventoryQualityEvents || accountingFinancialResult
? "business_overview_from_confirmed_1c_multi_family_rows"
: debtOpenSettlementQuality || debtDueDateAging
? "business_overview_from_confirmed_1c_multi_family_rows"
@@ -3343,6 +3425,9 @@ function summarizeBusinessOverviewRows(input) {
if (input.inventoryAgingResult && !input.inventoryAgingResult.error) {
parts.push(`${input.inventoryAgingResult.fetched_rows} inventory aging rows fetched, ${input.inventoryAgingResult.matched_rows} matched`);
}
if (input.inventoryQualityEventsResult && !input.inventoryQualityEventsResult.error) {
parts.push(`${input.inventoryQualityEventsResult.fetched_rows} inventory quality-event rows fetched, ${input.inventoryQualityEventsResult.matched_rows} matched`);
}
return parts.length > 0 ? parts.join("; ") : null;
}
function buildBusinessOverviewConfirmedFacts(derived) {
@@ -3511,6 +3596,18 @@ function buildBusinessOverviewConfirmedFacts(derived) {
const proxy = derived.inventory_staleness_risk_proxy;
facts.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`);
}
if (derived.inventory_quality_events) {
const quality = derived.inventory_quality_events;
const eventWindow = quality.first_event_date && quality.latest_event_date
? ` Окно найденных событий: ${quality.first_event_date} — ${quality.latest_event_date}.`
: "";
if (quality.evidence_status === "reviewed_no_quality_events_found") {
facts.push(`Reviewed inventory quality route проверил складские документы списания, оприходования, инвентаризации и переоценки${period}: подтвержденных событий не найдено. Это проверенный отрицательный результат по доступным документам, но не рыночная ликвидационная оценка и не управленческий резерв.`);
}
else {
facts.push(`Reviewed inventory quality route проверил складские документы${period}: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С, но не самостоятельная рыночная ликвидационная стоимость.`);
}
}
return facts;
}
function buildBusinessOverviewInferredFacts(derived) {
@@ -4218,6 +4315,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
let contractUsageProfileResult = null;
let inventoryOnHandResult = null;
let inventoryAgingResult = null;
let inventoryQualityEventsResult = null;
const valueFilters = buildValueFlowFilters(planner);
const lifecycleFilters = buildLifecycleFilters(planner);
const profileFilters = buildBusinessOverviewProfileFilters(planner);
@@ -4227,6 +4325,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const debtFilters = buildBusinessOverviewDebtFilters(planner);
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
const inventoryQualityEventsProbeEnabled = shouldRunInventoryQualityEventsProbe(planner);
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
const incomingSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("customer_revenue_and_payments", valueFilters);
@@ -4262,6 +4361,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const inventoryAgingSelection = inventoryFilters
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_aging_by_purchase_date", inventoryFilters)
: null;
const inventoryQualityEventsSelection = inventoryQualityEventsProbeEnabled
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_quality_events_for_organization", inventoryFilters ?? buildBusinessOverviewProfileFilters(planner))
: null;
if (!incomingSelection.selected_recipe || !outgoingSelection.selected_recipe || !lifecycleSelection.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_recipe_not_available");
const missing = [
@@ -4389,6 +4491,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
pushReason(reasonCodes, "pilot_business_overview_inventory_recipe_not_available");
pushUnique(queryLimitations, "Business overview inventory-position probe requires an executable inventory on-hand as-of-date recipe");
}
if (inventoryQualityEventsSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_recipe_selected");
}
else if (!inventoryQualityEventsProbeEnabled) {
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_probe_skipped_without_boundary_need");
}
else {
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_recipe_not_available");
pushUnique(queryLimitations, "Business overview inventory quality probe requires an executable inventory quality-events recipe");
}
for (const step of dryRun.execution_steps) {
if (step.primitive_id === "query_movements") {
const incomingExecution = await executeCoverageAwareValueFlowQuery({
@@ -4622,6 +4734,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
});
probeResults.push(queryResultToProbeResult(step.primitive_id, contractUsageProfileResult));
}
if (inventoryQualityEventsSelection?.selected_recipe) {
const inventoryQualityEventsFilters = inventoryFilters ?? buildBusinessOverviewProfileFilters(planner);
const inventoryQualityEventsPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryQualityEventsSelection.selected_recipe, inventoryQualityEventsFilters);
inventoryQualityEventsResult = await runtimeDeps.executeAddressMcpQuery({
query: inventoryQualityEventsPlan.query,
limit: inventoryQualityEventsPlan.limit,
account_scope: inventoryQualityEventsPlan.account_scope
});
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryQualityEventsResult));
}
if (lifecycleResult.error) {
pushUnique(queryLimitations, lifecycleResult.error);
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error");
@@ -4657,6 +4779,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
else if (contractUsageProfileResult) {
pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_executed");
}
if (inventoryQualityEventsResult?.error) {
pushUnique(queryLimitations, inventoryQualityEventsResult.error);
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_query_mcp_error");
}
else if (inventoryQualityEventsResult) {
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_query_mcp_executed");
}
continue;
}
skippedPrimitives.push(step.primitive_id);
@@ -4679,6 +4808,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
debtAsOfDate,
inventoryOnHandResult,
inventoryAgingResult,
inventoryQualityEventsResult,
inventoryAsOfDate,
organizationScope,
periodScope: dateScope
@@ -4744,6 +4874,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (derivedBusinessOverview.inventory_staleness_risk_proxy) {
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows");
}
if (derivedBusinessOverview.inventory_quality_events) {
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_quality_events_from_reviewed_rows");
pushReason(reasonCodes, `pilot_derived_business_overview_inventory_quality_events_${derivedBusinessOverview.inventory_quality_events.evidence_status}`);
}
if (derivedBusinessOverview.missing_proof_families.length > 0) {
pushReason(reasonCodes, "pilot_business_overview_missing_proof_families_recorded");
}
@@ -4763,7 +4897,8 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
counterpartyProfileResult,
contractUsageProfileResult,
inventoryOnHandResult,
inventoryAgingResult
inventoryAgingResult,
inventoryQualityEventsResult
});
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
@@ -830,10 +830,18 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
}
if (inventoryReserveBoundary) {
const headline = toNonEmptyString(draft.headline);
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя.");
if (inventoryQualityEvents) {
if (limitLine) {
lines.push(limitLine);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const boundaryLines = userFacingLines([
...toStringList(draft.unknown_lines),
...toStringList(draft.limitation_lines)