Закрыть 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
@@ -296,6 +296,62 @@ 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__
БанкСписание.Дата КАК Период,
@@ -958,6 +1014,16 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
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",
@@ -1432,6 +1498,77 @@ function buildInventoryMovementQuery(
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
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: AddressFilterSet,
dateFieldPath: string,
organizationFieldPath: string,
warehouseFieldPath: string
): string {
return buildWhereClause(filters, dateFieldPath, [
`${dateFieldPath.replace(/\.Дата$/u, ".Проведен")} = ИСТИНА`,
buildOrganizationReferenceCondition(filters, [organizationFieldPath]),
buildWarehouseReferenceCondition(filters, [warehouseFieldPath])
].filter((item): item is string => Boolean(item)));
}
function buildInventoryQualityEventsQuery(filters: AddressFilterSet, resolvedLimit: number): string {
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: AddressFilterSet, fieldPaths: string[]): string | null {
const item = typeof filters.item === "string" ? filters.item.trim() : "";
if (!item) {
@@ -1783,6 +1920,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
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" ||
@@ -2037,6 +2175,8 @@ export function buildAddressRecipePlan(
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
: 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"
@@ -510,6 +510,9 @@ function isVendorRiskBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionCont
}
function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
if (overview.inventory_quality_events) {
return "рыночная ликвидационная стоимость и управленческий резерв склада";
}
if (overview.inventory_staleness_risk_proxy) {
return "резервы/списания/ликвидационная стоимость склада";
}
@@ -764,6 +767,26 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
}
function businessOverviewInventoryQualityEventsText(overview: BusinessOverview): string | null {
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: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
@@ -797,6 +820,10 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
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
@@ -870,6 +897,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
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 ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
@@ -1101,6 +1131,9 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
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)
@@ -1676,6 +1709,10 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
`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;
}
@@ -1858,6 +1895,16 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
`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;
@@ -1873,7 +1920,8 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin
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
@@ -2020,6 +2068,10 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
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");
}
@@ -265,6 +265,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
inventory_quality_events: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents | null;
document_activity_profile: AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null;
counterparty_profile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null;
contract_usage_profile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null;
@@ -521,6 +522,26 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessR
inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows";
}
export interface AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents {
period_scope: string | null;
rows_matched: number;
writeoff_rows: number;
writeoff_amount: number;
writeoff_amount_human_ru: string;
receipt_adjustment_rows: number;
receipt_adjustment_amount: number;
receipt_adjustment_amount_human_ru: string;
inventory_count_rows: number;
revaluation_rows: number;
first_event_date: string | null;
latest_event_date: string | null;
evidence_status:
| "reviewed_no_quality_events_found"
| "reviewed_writeoff_or_adjustment_events_found"
| "reviewed_inventory_control_events_only";
inference_basis: "inventory_quality_documents_confirmed_1c_rows";
}
export interface AssistantMcpDiscoveryDerivedMetadataSurface {
metadata_scope: string | null;
requested_meta_types: string[];
@@ -829,6 +850,17 @@ function shouldRunDebtDueDateAgingProbe(planner: AssistantMcpDiscoveryPlannerCon
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
}
function shouldRunInventoryQualityEventsProbe(planner: AssistantMcpDiscoveryPlannerContract): boolean {
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): item is string => Boolean(item))
.join(" ");
return /(?:inventory_reserve|reserve_liquidation|liquidation|write[-_ ]?off|obsolete|obsolescence|inventory_reserve_liquidation_quality|резерв|списан|ликвидац|неликвид|обесцен)/iu.test(combined);
}
function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null {
const meaning = planner.discovery_plan.turn_meaning_ref;
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
@@ -4148,6 +4180,79 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input: {
};
}
function rowInventoryQualityEventType(row: Record<string, unknown>): string {
return rowTextValue(row, ["ТипСобытия", "EventType", "event_type", "Регистратор", "Registrator", "registrator"]) ?? "";
}
function deriveBusinessOverviewInventoryQualityEvents(input: {
inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null;
periodScope: string | null;
}): AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents | null {
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: string[] = [];
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: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents["evidence_status"] =
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: {
rankedOutgoing: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
outgoing: AssistantMcpDiscoveryValueFlowSideSummary;
@@ -4226,6 +4331,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
inventoryQualityEvents: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents | null;
vendorProcurementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null;
hasSupplierConcentrationSignal: boolean;
}): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily[] {
@@ -4267,12 +4373,12 @@ function buildBusinessOverviewMissingProofFamilies(input: {
});
}
if (
if ((
missing.has("inventory_position") ||
missing.has("inventory_turnover_quality") ||
missing.has("inventory_liquidity_quality") ||
missing.has("inventory_reserve_liquidation_quality")
) {
) && !input.inventoryQualityEvents) {
pushUnique({
family: "inventory_reserve_liquidation_quality",
current_status: input.inventoryStalenessRiskProxy
@@ -4322,6 +4428,7 @@ function deriveBusinessOverview(input: {
debtAsOfDate: string | null;
inventoryOnHandResult: AddressMcpQueryExecutorResult | null;
inventoryAgingResult: AddressMcpQueryExecutorResult | null;
inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null;
inventoryAsOfDate: string | null;
organizationScope: string | null;
periodScope: string | null;
@@ -4390,6 +4497,10 @@ function deriveBusinessOverview(input: {
inventoryPosition,
inventoryTurnoverProxy
});
const inventoryQualityEvents = deriveBusinessOverviewInventoryQualityEvents({
inventoryQualityEventsResult: input.inventoryQualityEventsResult,
periodScope: input.periodScope
});
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
rankedOutgoing,
outgoing,
@@ -4414,6 +4525,7 @@ function deriveBusinessOverview(input: {
Boolean(inventoryPosition),
Boolean(inventoryTurnoverProxy),
Boolean(inventoryStalenessRiskProxy),
Boolean(inventoryQualityEvents),
Boolean(vendorProcurementQuality)
].filter(Boolean).length;
if (checkedSignalCount <= 0) {
@@ -4430,7 +4542,9 @@ function deriveBusinessOverview(input: {
debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
taxPosition ? null : "tax_position",
inventoryPosition
? inventoryStalenessRiskProxy
? inventoryQualityEvents
? null
: inventoryStalenessRiskProxy
? "inventory_reserve_liquidation_quality"
: inventoryTurnoverProxy
? "inventory_liquidity_quality"
@@ -4448,6 +4562,7 @@ function deriveBusinessOverview(input: {
inventoryPosition,
inventoryTurnoverProxy,
inventoryStalenessRiskProxy,
inventoryQualityEvents,
vendorProcurementQuality,
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
});
@@ -4473,6 +4588,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,
@@ -4483,7 +4599,7 @@ function deriveBusinessOverview(input: {
missing_signal_families: missingSignalFamilies,
missing_proof_families: missingProofFamilies,
inference_basis:
hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
hasBusinessOverviewProfileSignal || inventoryPosition || inventoryQualityEvents || accountingFinancialResult
? "business_overview_from_confirmed_1c_multi_family_rows"
: debtOpenSettlementQuality || debtDueDateAging
? "business_overview_from_confirmed_1c_multi_family_rows"
@@ -4513,6 +4629,7 @@ function summarizeBusinessOverviewRows(input: {
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
inventoryOnHandResult: AddressMcpQueryExecutorResult | null;
inventoryAgingResult: AddressMcpQueryExecutorResult | null;
inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null;
}): string | null {
const parts: string[] = [];
if (input.incomingResult && !input.incomingResult.error) {
@@ -4560,6 +4677,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;
}
@@ -4780,6 +4900,22 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
`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;
}
@@ -5616,6 +5752,7 @@ export async function executeAssistantMcpDiscoveryPilot(
let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null;
let inventoryOnHandResult: AddressMcpQueryExecutorResult | null = null;
let inventoryAgingResult: AddressMcpQueryExecutorResult | null = null;
let inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null = null;
const valueFilters = buildValueFlowFilters(planner);
const lifecycleFilters = buildLifecycleFilters(planner);
const profileFilters = buildBusinessOverviewProfileFilters(planner);
@@ -5625,6 +5762,7 @@ export async function executeAssistantMcpDiscoveryPilot(
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 = selectAddressRecipe("customer_revenue_and_payments", valueFilters);
@@ -5660,6 +5798,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const inventoryAgingSelection = inventoryFilters
? selectAddressRecipe("inventory_aging_by_purchase_date", inventoryFilters)
: null;
const inventoryQualityEventsSelection = inventoryQualityEventsProbeEnabled
? 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");
@@ -5771,6 +5912,14 @@ export async function executeAssistantMcpDiscoveryPilot(
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({
@@ -6007,6 +6156,19 @@ export async function executeAssistantMcpDiscoveryPilot(
});
probeResults.push(queryResultToProbeResult(step.primitive_id, contractUsageProfileResult));
}
if (inventoryQualityEventsSelection?.selected_recipe) {
const inventoryQualityEventsFilters = inventoryFilters ?? buildBusinessOverviewProfileFilters(planner);
const inventoryQualityEventsPlan = 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");
@@ -6037,6 +6199,12 @@ export async function executeAssistantMcpDiscoveryPilot(
} 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;
}
@@ -6061,6 +6229,7 @@ export async function executeAssistantMcpDiscoveryPilot(
debtAsOfDate,
inventoryOnHandResult,
inventoryAgingResult,
inventoryQualityEventsResult,
inventoryAsOfDate,
organizationScope,
periodScope: dateScope
@@ -6126,6 +6295,10 @@ export async function executeAssistantMcpDiscoveryPilot(
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");
}
@@ -6145,7 +6318,8 @@ export async function executeAssistantMcpDiscoveryPilot(
counterpartyProfileResult,
contractUsageProfileResult,
inventoryOnHandResult,
inventoryAgingResult
inventoryAgingResult,
inventoryQualityEventsResult
});
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
@@ -997,12 +997,20 @@ function buildCompactBusinessOverviewReply(
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)
@@ -36,6 +36,7 @@ export type AddressIntent =
| "inventory_profitability_for_item"
| "inventory_purchase_to_sale_chain"
| "inventory_aging_by_purchase_date"
| "inventory_quality_events_for_organization"
| "account_balance_snapshot"
| "open_items_by_counterparty_or_contract"
| "list_documents_by_counterparty"
@@ -204,7 +205,8 @@ export interface AddressRecipeDefinition {
| "inventory_trading_margin_proxy_profile"
| "inventory_profitability_profile"
| "inventory_purchase_to_sale_chain_profile"
| "inventory_aging_by_purchase_date_profile";
| "inventory_aging_by_purchase_date_profile"
| "inventory_quality_events_profile";
required_filters: Array<keyof AddressFilterSet>;
optional_filters: Array<keyof AddressFilterSet>;
default_limit: number;