Open-World: добавить торговый margin proxy в бизнес-обзор
This commit is contained in:
parent
822baedcba
commit
062655eca0
|
|
@ -24,8 +24,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now
|
|||
- Completed active slice: `Selected-Item Profitability Route Bridge`: selected-object inventory profitability now has a bounded exact recipe over purchase/sale document rows, with explicit boundaries that this is a gross spread/margin proxy rather than company net profit.
|
||||
- Completed active slice: `Business Overview Contract-Date Debt Age Signal Bridge`: explicit-period open-settlement quality can now include contract-date age as a bounded signal, while due-date aging/overdue debt remains unclaimed until a reviewed payment-term route exists.
|
||||
- Completed active slice: `Business Overview Analyst Synthesis Layer`: business-overview answers now turn checked fact families into a bounded analyst note with operating scale, customer concentration, risk contours, and explicit profit/margin boundaries.
|
||||
- Next active slice: continue breadth into company-wide profit/margin, real due-date debt aging, inventory-liquidity/turnover, and broader unfamiliar 1C route families only where reviewed evidence routes exist.
|
||||
- Active module progress: `~73% (Open-World Bounded Autonomy Breadth)`.
|
||||
- Completed active slice: `Business Overview Trading Margin Proxy Bridge`: explicit-period business overview can include a bounded товарный sales-vs-purchase document proxy for revenue, purchase-cost trace, gross spread, and margin proxy, while clean profit/accounting финрезультат remains unclaimed.
|
||||
- Next active slice: continue breadth into exact company-wide accounting profit/margin, real due-date debt aging, inventory-liquidity/turnover, and broader unfamiliar 1C route families only where reviewed evidence routes exist.
|
||||
- Active module progress: `~77% (Open-World Bounded Autonomy Breadth)`.
|
||||
|
||||
## Reporting Rule
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ The project is not yet a universal arbitrary-1C agent.
|
|||
|
||||
Remaining work belongs to the next breadth module:
|
||||
|
||||
- extend `business_overview` beyond money-flow/activity, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, and as-of-date inventory position into separately proven company-wide profit/margin, due-date debt aging/overdue, and real inventory-liquidity evidence families;
|
||||
- extend `business_overview` beyond money-flow/activity, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, as-of-date inventory position, and trading-margin proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, and real inventory-liquidity evidence families;
|
||||
- broader dynamic schema traversal for unfamiliar 1C asks;
|
||||
- more primitive descriptors where live evidence proves a real gap;
|
||||
- more replay-backed domain packs that start from user business meaning, not from route convenience;
|
||||
|
|
|
|||
|
|
@ -247,11 +247,33 @@ Local validation is accepted for this slice:
|
|||
- `npm.cmd test -- assistantMcp`: passed `305/305` with `9` skipped.
|
||||
- `npm.cmd run build`: passed.
|
||||
|
||||
## Slice 11 - Business Overview Trading Margin Proxy Bridge
|
||||
|
||||
This slice opens a company-period trading-margin proxy lane where the evidence is concrete enough, while keeping the accounting boundary explicit.
|
||||
|
||||
It does not claim clean profit, бухгалтерский финрезультат, or exact cost-of-sales margin.
|
||||
|
||||
Implemented now:
|
||||
|
||||
- `inventory_trading_margin_proxy_for_organization` exists as a bounded recipe over the reviewed purchase/sale document union on `41.01`;
|
||||
- explicit-period business overview can execute this document probe beside money-flow/activity, VAT/tax, debt-position, open-settlement quality, and inventory probes;
|
||||
- the pilot derives sales revenue, purchase-document cost proxy, gross spread proxy, margin-to-revenue, markup-to-purchase-cost, and top items only from confirmed товарные document rows;
|
||||
- the answer adapter surfaces this as `торговый margin proxy`, not as чистая прибыль or accounting margin;
|
||||
- the unknown family narrows from generic `profit_margin` to `accounting_profit_margin` when the proxy exists, so the user sees both what is now supported and what remains unproved;
|
||||
- all-time business overview does not silently reuse the prior period's trading proxy, because the probe requires a fresh explicit period window.
|
||||
|
||||
This is a material breadth step for broad company analysis, but it is still not full company-wide profit analysis. Exact profit/margin still needs reviewed evidence for себестоимость продаж, expenses, period closing, and accounting result.
|
||||
|
||||
Local validation is accepted for this slice:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts addressQueryRuntimeM23.test.ts`: passed `478` with `1` skipped.
|
||||
- `npm.cmd run build`: passed.
|
||||
|
||||
### Still Pending Breadth Slices
|
||||
|
||||
Grow this bridge beyond the first confirmed signal bundle:
|
||||
|
||||
- add separate evidence families for company-wide profit/margin and due-date debt aging/overdue quality where reviewed due-date/payment-term routes exist;
|
||||
- add separate evidence families for exact company-wide accounting profit/margin and due-date debt aging/overdue quality where reviewed closing-cost and due-date/payment-term routes exist;
|
||||
- extend inventory evidence from as-of-date stock position into real turnover/liquidity only when reviewed sales velocity, aging, or obsolescence evidence exists;
|
||||
- upgrade debt evidence from as-of-date position/open-settlement concentration/contract-date age into overdue aging only when reviewed due-date or payment-term aging evidence exists;
|
||||
- extend VAT/tax beyond explicit-period tax position only when the requested tax fact is provable and the period is explicit;
|
||||
|
|
@ -264,7 +286,7 @@ The slice is healthy when:
|
|||
- broad analysis wording lands on the business-overview contour;
|
||||
- the answer is materially more informative than a generic recap;
|
||||
- confirmed metrics are visibly separated from LLM-style interpretation;
|
||||
- profit/margin are not claimed without supporting evidence;
|
||||
- profit/margin are not claimed without supporting evidence, and trading-margin proxy is visibly bounded away from clean profit/accounting финрезультат;
|
||||
- Post-F stale-scope and phase83 catalog-alignment canaries remain green.
|
||||
|
||||
## Validation
|
||||
|
|
@ -339,3 +361,10 @@ Business-overview analyst synthesis validation:
|
|||
- `npm.cmd run build`: passed.
|
||||
|
||||
Graphify rebuild after Slice 10 code/doc sync: `6023 nodes`, `13112 edges`, `136 communities`.
|
||||
|
||||
Business-overview trading-margin proxy validation:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts addressQueryRuntimeM23.test.ts`: passed `478` with `1` skipped.
|
||||
- `npm.cmd run build`: passed.
|
||||
|
||||
Graphify rebuild after Slice 11 code/doc sync: `6028 nodes`, `13131 edges`, `137 communities`.
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ Status canon for planning:
|
|||
- The current completed breadth slice is `Selected-Item Profitability Route Bridge`: selected-object inventory profitability now has a bounded exact route over purchase/sale document rows and reports gross spread/margin proxy without claiming company net profit.
|
||||
- The current completed breadth slice is `Business Overview Contract-Date Debt Age Signal Bridge`: explicit-period open-settlement quality can include contract-date age as a bounded signal, while due-date aging/overdue debt still waits for reviewed payment-term evidence.
|
||||
- The current completed breadth slice is `Business Overview Analyst Synthesis Layer`: broad company-analysis answers now synthesize checked fact families into operating scale, customer concentration, risk contours, and a concise bounded LLM-audit.
|
||||
- The next active breadth slice continues breadth into company-wide profit/margin, real due-date debt aging, inventory-liquidity/turnover, and broader unfamiliar 1C route families without relaxing truth boundaries.
|
||||
- The current completed breadth slice is `Business Overview Trading Margin Proxy Bridge`: explicit-period company analysis can now include товарный sales-vs-purchase document proxy for revenue, purchase-cost trace, gross spread, and margin proxy, while clean profit/accounting финрезультат remains unclaimed.
|
||||
- The next active breadth slice continues breadth into exact company-wide accounting profit/margin, real due-date debt aging, inventory-liquidity/turnover, and broader unfamiliar 1C route families without relaxing truth boundaries.
|
||||
- The short source of truth for status wording is [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md).
|
||||
|
||||
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved through bounded MCP autonomy, Post-F hardening, inventory breadth proof, and the declared Planner Autonomy slice:
|
||||
|
|
@ -124,11 +125,11 @@ Current honest status:
|
|||
- pre-multidomain readiness: `~90%`
|
||||
- bounded-autonomy foundation readiness: `~89%`
|
||||
- open-world bounded-autonomy readiness: `~87%`
|
||||
- active Open-World Bounded Autonomy Breadth progress: `~73%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, and analyst synthesis added to business-overview answer drafting; company-wide profit/margin, true due-date debt aging/overdue, and real inventory-liquidity expansion are still pending
|
||||
- active Open-World Bounded Autonomy Breadth progress: `~77%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, analyst synthesis added to business-overview answer drafting, and company-period trading margin proxy bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, and real inventory-liquidity expansion are still pending
|
||||
- Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice
|
||||
- active inventory-stock breadth slice progress: `100%` for the declared scenario pack, not for arbitrary inventory questions
|
||||
- Planner Autonomy Consolidation progress: `100%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, unambiguous metadata-surface lane inference, catalog chain-template scoring, structured chain-match contract exposure, runtime/debug propagation, subject-aware bidirectional comparison arbitration, structured catalog-alignment verdicts, representative alignment regression guard, catalog-alignment reason-code telemetry, explicit `alignment_status` propagation, truth-harness/acceptance-matrix surfacing, soft divergence warning, `catalog_alignment_ok` acceptance invariant, step-level expected catalog-alignment assertions, phase66 and phase32 spec alignment expectations, AGENT source-catalog surfacing, generated phase83 mixed planner-brain replay spec, checked-source user-facing error sanitation, surface-grounded catalog promotion, and guarded live phase83 acceptance validated. Broader unfamiliar 1C asks are now next-module breadth work rather than an open blocker inside this declared slice
|
||||
- graph snapshot after latest rebuild: `6023 nodes`, `13112 edges`, `136 communities`
|
||||
- graph snapshot after latest rebuild: `6028 nodes`, `13131 edges`, `137 communities`
|
||||
- current regression-gate breakpoint:
|
||||
- the validated hot paths are no longer structurally broken;
|
||||
- flagship continuity collapse is no longer the primary risk;
|
||||
|
|
|
|||
|
|
@ -833,6 +833,17 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "inventory_sale_trace_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_trading_margin_proxy_for_organization_v1",
|
||||
intent: "inventory_trading_margin_proxy_for_organization",
|
||||
purpose: "Trace organization-period purchase and sale document rows and derive bounded trading revenue, purchase-cost proxy, spread, and margin proxy",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 600,
|
||||
account_scope: ["41.01"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_trading_margin_proxy_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
|
|
@ -1315,6 +1326,7 @@ function maxLimitForIntent(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -1498,31 +1510,15 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_profitability_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? 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_purchase_to_sale_chain_profile"
|
||||
? 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)
|
||||
|
|
@ -1534,13 +1530,13 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__PAYABLE_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)
|
||||
|
|
@ -1552,23 +1548,41 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__PAYABLE_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 RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__RECEIVABLE_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,
|
||||
|
|
|
|||
|
|
@ -349,6 +349,9 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
families.push("долговой срез на дату");
|
||||
}
|
||||
|
|
@ -361,7 +364,7 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
const unknownFamilies = ["прибыль/маржа"];
|
||||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
if (!overview.tax_position) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
|
|
@ -547,6 +550,7 @@ function buildMustNotClaim(pilot) {
|
|||
}
|
||||
if (isBusinessOverviewPilot(pilot)) {
|
||||
claims.push("Do not present business overview cash-flow spread as profit or margin.");
|
||||
claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
|
||||
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
|
||||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||
|
|
@ -865,6 +869,11 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
: "сбалансирован";
|
||||
lines.push(`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`);
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
lines.push(`Торговый margin proxy за ${proxy.period_scope}: выручка продаж ${proxy.sales_revenue_human_ru}, закупочный документный след ${proxy.purchase_cost_proxy_human_ru}, валовый спред proxy ${proxy.gross_spread_proxy_human_ru}, маржинальность к выручке ${marginText}. Это не чистая прибыль и не бухгалтерский финрезультат.`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
? "в пользу дебиторки"
|
||||
|
|
@ -934,6 +943,12 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
: "НДС-позиция сбалансирована";
|
||||
signals.push(taxDirection);
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const marginText = overview.trading_margin_proxy.margin_to_revenue_pct === null
|
||||
? "маржинальность не рассчитана"
|
||||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||||
|
|
@ -962,6 +977,7 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
function businessOverviewExecutiveVerdictLine(overview) {
|
||||
const hasCash = overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0;
|
||||
const hasExtraSignals = Boolean(overview.tax_position ||
|
||||
overview.trading_margin_proxy ||
|
||||
overview.debt_position ||
|
||||
overview.debt_open_settlement_quality ||
|
||||
overview.inventory_position);
|
||||
|
|
@ -1038,6 +1054,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.derived_business_overview?.tax_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||||
}
|
||||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,6 +205,21 @@ function buildBusinessOverviewInventoryFilters(planner) {
|
|||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function buildBusinessOverviewTradingMarginFilters(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
|
||||
const periodFilters = dateScopeToFilters(dateScope);
|
||||
if (!periodFilters.period_from || !periodFilters.period_to) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...periodFilters,
|
||||
...(organization ? { organization } : {}),
|
||||
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
function buildInventoryExactFilters(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const subject = firstEntityCandidate(planner);
|
||||
|
|
@ -1454,6 +1469,16 @@ function rowDocumentValue(row) {
|
|||
function rowAccountValue(row) {
|
||||
return rowTextValue(row, ["СчетДт", "AccountDt", "account_dt", "Счет", "Account", "account"]);
|
||||
}
|
||||
function rowDebitAccountValue(row) {
|
||||
return rowTextValue(row, ["СчетДт", "AccountDt", "account_dt", "DebitAccount", "debit_account"]);
|
||||
}
|
||||
function rowCreditAccountValue(row) {
|
||||
return rowTextValue(row, ["СчетКт", "AccountKt", "account_kt", "CreditAccount", "credit_account"]);
|
||||
}
|
||||
function accountTextMatchesPrefix(account, prefixes) {
|
||||
const normalized = String(account ?? "").replace(/\s+/g, "");
|
||||
return prefixes.some((prefix) => normalized.startsWith(prefix));
|
||||
}
|
||||
function rowQuantityValue(row) {
|
||||
return rowNumberValue(row, ["Количество", "Quantity", "quantity", "Qty", "qty", "Остаток", "Balance", "balance"]);
|
||||
}
|
||||
|
|
@ -1961,6 +1986,87 @@ function deriveBusinessOverviewTaxPosition(result, periodScope) {
|
|||
inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows"
|
||||
};
|
||||
}
|
||||
function deriveBusinessOverviewTradingMarginProxy(result, periodScope) {
|
||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||
return null;
|
||||
}
|
||||
const itemBuckets = new Map();
|
||||
let salesRowsWithAmount = 0;
|
||||
let purchaseRowsWithAmount = 0;
|
||||
let salesRevenue = 0;
|
||||
let purchaseCostProxy = 0;
|
||||
for (const row of result.rows) {
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount === null || amount <= 0) {
|
||||
continue;
|
||||
}
|
||||
const isSale = accountTextMatchesPrefix(rowCreditAccountValue(row), ["41.01"]);
|
||||
const isPurchase = accountTextMatchesPrefix(rowDebitAccountValue(row), ["41.01"]);
|
||||
if (!isSale && !isPurchase) {
|
||||
continue;
|
||||
}
|
||||
const item = rowInventoryItemValue(row) ?? "unknown_item";
|
||||
const quantity = rowQuantityValue(row) ?? 0;
|
||||
const bucket = itemBuckets.get(item) ?? {
|
||||
sales_revenue: 0,
|
||||
purchase_cost_proxy: 0,
|
||||
sales_quantity: 0,
|
||||
purchase_quantity: 0
|
||||
};
|
||||
if (isSale) {
|
||||
salesRowsWithAmount += 1;
|
||||
salesRevenue += amount;
|
||||
bucket.sales_revenue += amount;
|
||||
bucket.sales_quantity += quantity;
|
||||
}
|
||||
if (isPurchase) {
|
||||
purchaseRowsWithAmount += 1;
|
||||
purchaseCostProxy += amount;
|
||||
bucket.purchase_cost_proxy += amount;
|
||||
bucket.purchase_quantity += quantity;
|
||||
}
|
||||
itemBuckets.set(item, bucket);
|
||||
}
|
||||
if (salesRowsWithAmount <= 0 && purchaseRowsWithAmount <= 0) {
|
||||
return null;
|
||||
}
|
||||
const grossSpreadProxy = salesRevenue - purchaseCostProxy;
|
||||
const marginToRevenuePct = salesRevenue > 0 ? percentageOfTotal(grossSpreadProxy, salesRevenue) : null;
|
||||
const markupToPurchasePct = purchaseCostProxy > 0 ? percentageOfTotal(grossSpreadProxy, purchaseCostProxy) : null;
|
||||
const topItemsBySales = Array.from(itemBuckets.entries())
|
||||
.map(([item, bucket]) => ({
|
||||
item,
|
||||
sales_revenue: bucket.sales_revenue,
|
||||
sales_revenue_human_ru: formatAmountHumanRu(bucket.sales_revenue),
|
||||
purchase_cost_proxy: bucket.purchase_cost_proxy,
|
||||
purchase_cost_proxy_human_ru: formatAmountHumanRu(bucket.purchase_cost_proxy),
|
||||
gross_spread_proxy: bucket.sales_revenue - bucket.purchase_cost_proxy,
|
||||
gross_spread_proxy_human_ru: formatAmountHumanRu(bucket.sales_revenue - bucket.purchase_cost_proxy),
|
||||
sales_quantity: bucket.sales_quantity,
|
||||
purchase_quantity: bucket.purchase_quantity
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const salesDelta = right.sales_revenue - left.sales_revenue;
|
||||
return salesDelta !== 0 ? salesDelta : left.item.localeCompare(right.item, "ru");
|
||||
})
|
||||
.slice(0, 5);
|
||||
return {
|
||||
period_scope: periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
sales_rows_with_amount: salesRowsWithAmount,
|
||||
purchase_rows_with_amount: purchaseRowsWithAmount,
|
||||
sales_revenue: salesRevenue,
|
||||
sales_revenue_human_ru: formatAmountHumanRu(salesRevenue),
|
||||
purchase_cost_proxy: purchaseCostProxy,
|
||||
purchase_cost_proxy_human_ru: formatAmountHumanRu(purchaseCostProxy),
|
||||
gross_spread_proxy: grossSpreadProxy,
|
||||
gross_spread_proxy_human_ru: formatAmountHumanRu(grossSpreadProxy),
|
||||
margin_to_revenue_pct: marginToRevenuePct,
|
||||
markup_to_purchase_pct: markupToPurchasePct,
|
||||
top_items_by_sales: topItemsBySales,
|
||||
inference_basis: "sales_documents_minus_purchase_documents_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
function deriveBusinessOverviewDebtSide(result) {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return {
|
||||
|
|
@ -2303,6 +2409,7 @@ function deriveBusinessOverview(input) {
|
|||
});
|
||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||
receivablesResult: input.receivablesResult,
|
||||
payablesResult: input.payablesResult,
|
||||
|
|
@ -2322,6 +2429,7 @@ function deriveBusinessOverview(input) {
|
|||
outgoing.rows_with_amount > 0,
|
||||
Boolean(activityPeriod),
|
||||
Boolean(taxPosition),
|
||||
Boolean(tradingMarginProxy),
|
||||
Boolean(debtPosition),
|
||||
Boolean(debtOpenSettlementQuality),
|
||||
Boolean(inventoryPosition)
|
||||
|
|
@ -2341,13 +2449,14 @@ function deriveBusinessOverview(input) {
|
|||
top_customers: rankedIncoming?.ranked_values ?? [],
|
||||
activity_period: activityPeriod,
|
||||
tax_position: taxPosition,
|
||||
trading_margin_proxy: tradingMarginProxy,
|
||||
debt_position: debtPosition,
|
||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||
inventory_position: inventoryPosition,
|
||||
coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
|
||||
checked_signal_count: checkedSignalCount,
|
||||
missing_signal_families: [
|
||||
"profit_margin",
|
||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
debtPosition ? null : "debt_position",
|
||||
debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
taxPosition ? null : "tax_position",
|
||||
|
|
@ -2381,6 +2490,9 @@ function summarizeBusinessOverviewRows(input) {
|
|||
if (input.taxResult && !input.taxResult.error) {
|
||||
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.tradingMarginResult && !input.tradingMarginResult.error) {
|
||||
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.receivablesResult && !input.receivablesResult.error) {
|
||||
parts.push(`${input.receivablesResult.fetched_rows} receivables rows fetched, ${input.receivablesResult.matched_rows} matched`);
|
||||
}
|
||||
|
|
@ -2426,6 +2538,11 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
|||
: "сбалансирован";
|
||||
facts.push(`НДС-позиция за ${derived.tax_position.period_scope} подтверждена по книгам продаж/покупок: продажи ${derived.tax_position.sales_vat_amount_human_ru}, покупки/вычеты ${derived.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${derived.tax_position.net_vat_amount_human_ru}.`);
|
||||
}
|
||||
if (derived.trading_margin_proxy) {
|
||||
const proxy = derived.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
facts.push(`Торговый margin proxy за ${proxy.period_scope} подтвержден по товарным документам продаж/поступлений: выручка продаж ${proxy.sales_revenue_human_ru}, закупочный документный след ${proxy.purchase_cost_proxy_human_ru}, валовый спред proxy ${proxy.gross_spread_proxy_human_ru}, маржинальность к выручке ${marginText}. Это не чистая прибыль и не бухгалтерский финрезультат.`);
|
||||
}
|
||||
if (derived.debt_position) {
|
||||
const debtDirection = derived.debt_position.net_debt_position_direction === "net_receivable"
|
||||
? "в пользу дебиторки"
|
||||
|
|
@ -2495,6 +2612,9 @@ function buildBusinessOverviewUnknownFacts(derived) {
|
|||
missing.has("profit_margin")
|
||||
? "Прибыль и маржа этим бизнес-обзором не подтверждены: нужны себестоимость, расходы и закрывающие документы."
|
||||
: null,
|
||||
missing.has("accounting_profit_margin")
|
||||
? "Чистая прибыль, бухгалтерская маржа и финрезультат этим бизнес-обзором не подтверждены: торговый proxy показывает только документную разницу продаж и поступлений без расходов, закрытия периода и точной себестоимости продаж."
|
||||
: null,
|
||||
missing.has("debt_quality")
|
||||
? "Качество дебиторки/кредиторки этим бизнес-обзором не подтверждено: нужен отдельный долговой срез."
|
||||
: null,
|
||||
|
|
@ -3123,6 +3243,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
let outgoingResult = null;
|
||||
let lifecycleResult = null;
|
||||
let taxResult = null;
|
||||
let tradingMarginResult = null;
|
||||
let receivablesResult = null;
|
||||
let payablesResult = null;
|
||||
let openContractsResult = null;
|
||||
|
|
@ -3131,6 +3252,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
const valueFilters = buildValueFlowFilters(planner);
|
||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
|
|
@ -3141,6 +3263,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
const taxSelection = taxFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||
: null;
|
||||
const tradingMarginSelection = tradingMarginFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||
: null;
|
||||
const receivablesSelection = debtFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("receivables_confirmed_as_of_date", debtFilters)
|
||||
: null;
|
||||
|
|
@ -3196,6 +3321,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
|
||||
}
|
||||
if (tradingMarginSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
||||
}
|
||||
else if (!tradingMarginFilters) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_probe_skipped_without_explicit_period");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview trading-margin proxy requires an executable explicit-period purchase/sale document recipe");
|
||||
}
|
||||
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_recipes_selected");
|
||||
}
|
||||
|
|
@ -3390,6 +3525,15 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
});
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, lifecycleResult));
|
||||
if (tradingMarginSelection?.selected_recipe) {
|
||||
const tradingMarginPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(tradingMarginSelection.selected_recipe, tradingMarginFilters);
|
||||
tradingMarginResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: tradingMarginPlan.query,
|
||||
limit: tradingMarginPlan.limit,
|
||||
account_scope: tradingMarginPlan.account_scope
|
||||
});
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, tradingMarginResult));
|
||||
}
|
||||
if (lifecycleResult.error) {
|
||||
pushUnique(queryLimitations, lifecycleResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error");
|
||||
|
|
@ -3397,6 +3541,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_executed");
|
||||
}
|
||||
if (tradingMarginResult?.error) {
|
||||
pushUnique(queryLimitations, tradingMarginResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_query_mcp_error");
|
||||
}
|
||||
else if (tradingMarginResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_query_mcp_executed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
|
|
@ -3407,6 +3558,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
|
|
@ -3428,6 +3580,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
if (derivedBusinessOverview.tax_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.debt_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_position_from_confirmed_rows");
|
||||
}
|
||||
|
|
@ -3446,6 +3601,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
|
|
|
|||
|
|
@ -857,6 +857,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "inventory_sale_trace_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_trading_margin_proxy_for_organization_v1",
|
||||
intent: "inventory_trading_margin_proxy_for_organization",
|
||||
purpose: "Trace organization-period purchase and sale document rows and derive bounded trading revenue, purchase-cost proxy, spread, and margin proxy",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 600,
|
||||
account_scope: ["41.01"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_trading_margin_proxy_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
|
|
@ -1438,6 +1449,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -1666,6 +1678,8 @@ export function buildAddressRecipePlan(
|
|||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_profitability_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
|
|
|
|||
|
|
@ -452,6 +452,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
families.push("долговой срез на дату");
|
||||
}
|
||||
|
|
@ -464,7 +467,7 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
const unknownFamilies = ["прибыль/маржа"];
|
||||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
if (!overview.tax_position) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
|
|
@ -658,6 +661,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
}
|
||||
if (isBusinessOverviewPilot(pilot)) {
|
||||
claims.push("Do not present business overview cash-flow spread as profit or margin.");
|
||||
claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
|
||||
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
|
||||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||
|
|
@ -1022,6 +1026,13 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
lines.push(
|
||||
`Торговый margin proxy за ${proxy.period_scope}: выручка продаж ${proxy.sales_revenue_human_ru}, закупочный документный след ${proxy.purchase_cost_proxy_human_ru}, валовый спред proxy ${proxy.gross_spread_proxy_human_ru}, маржинальность к выручке ${marginText}. Это не чистая прибыль и не бухгалтерский финрезультат.`
|
||||
);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection =
|
||||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
|
|
@ -1106,6 +1117,12 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
|||
: "НДС-позиция сбалансирована";
|
||||
signals.push(taxDirection);
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const marginText = overview.trading_margin_proxy.margin_to_revenue_pct === null
|
||||
? "маржинальность не рассчитана"
|
||||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection =
|
||||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
|
|
@ -1137,6 +1154,7 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin
|
|||
const hasCash = overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0;
|
||||
const hasExtraSignals = Boolean(
|
||||
overview.tax_position ||
|
||||
overview.trading_margin_proxy ||
|
||||
overview.debt_position ||
|
||||
overview.debt_open_settlement_quality ||
|
||||
overview.inventory_position
|
||||
|
|
@ -1223,6 +1241,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.derived_business_overview?.tax_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||||
}
|
||||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
|
|||
top_customers: AssistantMcpDiscoveryRankedValueFlowBucket[];
|
||||
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
|
||||
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
|
||||
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
|
||||
debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null;
|
||||
debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
|
||||
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||
|
|
@ -180,6 +181,35 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition {
|
|||
inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBusinessOverviewTradingItemBucket {
|
||||
item: string;
|
||||
sales_revenue: number;
|
||||
sales_revenue_human_ru: string;
|
||||
purchase_cost_proxy: number;
|
||||
purchase_cost_proxy_human_ru: string;
|
||||
gross_spread_proxy: number;
|
||||
gross_spread_proxy_human_ru: string;
|
||||
sales_quantity: number;
|
||||
purchase_quantity: number;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy {
|
||||
period_scope: string;
|
||||
rows_matched: number;
|
||||
sales_rows_with_amount: number;
|
||||
purchase_rows_with_amount: number;
|
||||
sales_revenue: number;
|
||||
sales_revenue_human_ru: string;
|
||||
purchase_cost_proxy: number;
|
||||
purchase_cost_proxy_human_ru: string;
|
||||
gross_spread_proxy: number;
|
||||
gross_spread_proxy_human_ru: string;
|
||||
margin_to_revenue_pct: number | null;
|
||||
markup_to_purchase_pct: number | null;
|
||||
top_items_by_sales: AssistantMcpDiscoveryBusinessOverviewTradingItemBucket[];
|
||||
inference_basis: "sales_documents_minus_purchase_documents_confirmed_1c_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBusinessOverviewDebtSideSummary {
|
||||
rows_matched: number;
|
||||
rows_with_amount: number;
|
||||
|
|
@ -578,6 +608,22 @@ function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPla
|
|||
};
|
||||
}
|
||||
|
||||
function buildBusinessOverviewTradingMarginFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
|
||||
const periodFilters = dateScopeToFilters(dateScope);
|
||||
if (!periodFilters.period_from || !periodFilters.period_to) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...periodFilters,
|
||||
...(organization ? { organization } : {}),
|
||||
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
sort: "period_asc"
|
||||
};
|
||||
}
|
||||
|
||||
function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const subject = firstEntityCandidate(planner);
|
||||
|
|
@ -2087,6 +2133,19 @@ function rowAccountValue(row: Record<string, unknown>): string | null {
|
|||
return rowTextValue(row, ["СчетДт", "AccountDt", "account_dt", "Счет", "Account", "account"]);
|
||||
}
|
||||
|
||||
function rowDebitAccountValue(row: Record<string, unknown>): string | null {
|
||||
return rowTextValue(row, ["СчетДт", "AccountDt", "account_dt", "DebitAccount", "debit_account"]);
|
||||
}
|
||||
|
||||
function rowCreditAccountValue(row: Record<string, unknown>): string | null {
|
||||
return rowTextValue(row, ["СчетКт", "AccountKt", "account_kt", "CreditAccount", "credit_account"]);
|
||||
}
|
||||
|
||||
function accountTextMatchesPrefix(account: string | null, prefixes: string[]): boolean {
|
||||
const normalized = String(account ?? "").replace(/\s+/g, "");
|
||||
return prefixes.some((prefix) => normalized.startsWith(prefix));
|
||||
}
|
||||
|
||||
function rowQuantityValue(row: Record<string, unknown>): number | null {
|
||||
return rowNumberValue(row, ["Количество", "Quantity", "quantity", "Qty", "qty", "Остаток", "Balance", "balance"]);
|
||||
}
|
||||
|
|
@ -2673,6 +2732,101 @@ function deriveBusinessOverviewTaxPosition(
|
|||
};
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewTradingMarginProxy(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
periodScope: string | null
|
||||
): AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null {
|
||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemBuckets = new Map<string, {
|
||||
sales_revenue: number;
|
||||
purchase_cost_proxy: number;
|
||||
sales_quantity: number;
|
||||
purchase_quantity: number;
|
||||
}>();
|
||||
let salesRowsWithAmount = 0;
|
||||
let purchaseRowsWithAmount = 0;
|
||||
let salesRevenue = 0;
|
||||
let purchaseCostProxy = 0;
|
||||
|
||||
for (const row of result.rows) {
|
||||
const amount = rowAmountValue(row);
|
||||
if (amount === null || amount <= 0) {
|
||||
continue;
|
||||
}
|
||||
const isSale = accountTextMatchesPrefix(rowCreditAccountValue(row), ["41.01"]);
|
||||
const isPurchase = accountTextMatchesPrefix(rowDebitAccountValue(row), ["41.01"]);
|
||||
if (!isSale && !isPurchase) {
|
||||
continue;
|
||||
}
|
||||
const item = rowInventoryItemValue(row) ?? "unknown_item";
|
||||
const quantity = rowQuantityValue(row) ?? 0;
|
||||
const bucket = itemBuckets.get(item) ?? {
|
||||
sales_revenue: 0,
|
||||
purchase_cost_proxy: 0,
|
||||
sales_quantity: 0,
|
||||
purchase_quantity: 0
|
||||
};
|
||||
if (isSale) {
|
||||
salesRowsWithAmount += 1;
|
||||
salesRevenue += amount;
|
||||
bucket.sales_revenue += amount;
|
||||
bucket.sales_quantity += quantity;
|
||||
}
|
||||
if (isPurchase) {
|
||||
purchaseRowsWithAmount += 1;
|
||||
purchaseCostProxy += amount;
|
||||
bucket.purchase_cost_proxy += amount;
|
||||
bucket.purchase_quantity += quantity;
|
||||
}
|
||||
itemBuckets.set(item, bucket);
|
||||
}
|
||||
|
||||
if (salesRowsWithAmount <= 0 && purchaseRowsWithAmount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const grossSpreadProxy = salesRevenue - purchaseCostProxy;
|
||||
const marginToRevenuePct = salesRevenue > 0 ? percentageOfTotal(grossSpreadProxy, salesRevenue) : null;
|
||||
const markupToPurchasePct = purchaseCostProxy > 0 ? percentageOfTotal(grossSpreadProxy, purchaseCostProxy) : null;
|
||||
const topItemsBySales = Array.from(itemBuckets.entries())
|
||||
.map(([item, bucket]) => ({
|
||||
item,
|
||||
sales_revenue: bucket.sales_revenue,
|
||||
sales_revenue_human_ru: formatAmountHumanRu(bucket.sales_revenue),
|
||||
purchase_cost_proxy: bucket.purchase_cost_proxy,
|
||||
purchase_cost_proxy_human_ru: formatAmountHumanRu(bucket.purchase_cost_proxy),
|
||||
gross_spread_proxy: bucket.sales_revenue - bucket.purchase_cost_proxy,
|
||||
gross_spread_proxy_human_ru: formatAmountHumanRu(bucket.sales_revenue - bucket.purchase_cost_proxy),
|
||||
sales_quantity: bucket.sales_quantity,
|
||||
purchase_quantity: bucket.purchase_quantity
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const salesDelta = right.sales_revenue - left.sales_revenue;
|
||||
return salesDelta !== 0 ? salesDelta : left.item.localeCompare(right.item, "ru");
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
return {
|
||||
period_scope: periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
sales_rows_with_amount: salesRowsWithAmount,
|
||||
purchase_rows_with_amount: purchaseRowsWithAmount,
|
||||
sales_revenue: salesRevenue,
|
||||
sales_revenue_human_ru: formatAmountHumanRu(salesRevenue),
|
||||
purchase_cost_proxy: purchaseCostProxy,
|
||||
purchase_cost_proxy_human_ru: formatAmountHumanRu(purchaseCostProxy),
|
||||
gross_spread_proxy: grossSpreadProxy,
|
||||
gross_spread_proxy_human_ru: formatAmountHumanRu(grossSpreadProxy),
|
||||
margin_to_revenue_pct: marginToRevenuePct,
|
||||
markup_to_purchase_pct: markupToPurchasePct,
|
||||
top_items_by_sales: topItemsBySales,
|
||||
inference_basis: "sales_documents_minus_purchase_documents_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewDebtSide(
|
||||
result: AddressMcpQueryExecutorResult | null
|
||||
): AssistantMcpDiscoveryBusinessOverviewDebtSideSummary {
|
||||
|
|
@ -3059,6 +3213,7 @@ function deriveBusinessOverview(input: {
|
|||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
||||
taxResult: AddressMcpQueryExecutorResult | null;
|
||||
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
||||
receivablesResult: AddressMcpQueryExecutorResult | null;
|
||||
payablesResult: AddressMcpQueryExecutorResult | null;
|
||||
openContractsResult: AddressMcpQueryExecutorResult | null;
|
||||
|
|
@ -3079,6 +3234,7 @@ function deriveBusinessOverview(input: {
|
|||
});
|
||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||
receivablesResult: input.receivablesResult,
|
||||
payablesResult: input.payablesResult,
|
||||
|
|
@ -3098,6 +3254,7 @@ function deriveBusinessOverview(input: {
|
|||
outgoing.rows_with_amount > 0,
|
||||
Boolean(activityPeriod),
|
||||
Boolean(taxPosition),
|
||||
Boolean(tradingMarginProxy),
|
||||
Boolean(debtPosition),
|
||||
Boolean(debtOpenSettlementQuality),
|
||||
Boolean(inventoryPosition)
|
||||
|
|
@ -3118,6 +3275,7 @@ function deriveBusinessOverview(input: {
|
|||
top_customers: rankedIncoming?.ranked_values ?? [],
|
||||
activity_period: activityPeriod,
|
||||
tax_position: taxPosition,
|
||||
trading_margin_proxy: tradingMarginProxy,
|
||||
debt_position: debtPosition,
|
||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||
inventory_position: inventoryPosition,
|
||||
|
|
@ -3125,7 +3283,7 @@ function deriveBusinessOverview(input: {
|
|||
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
|
||||
checked_signal_count: checkedSignalCount,
|
||||
missing_signal_families: [
|
||||
"profit_margin",
|
||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
debtPosition ? null : "debt_position",
|
||||
debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
taxPosition ? null : "tax_position",
|
||||
|
|
@ -3152,6 +3310,7 @@ function summarizeBusinessOverviewRows(input: {
|
|||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
lifecycleResult: AddressMcpQueryExecutorResult | null;
|
||||
taxResult: AddressMcpQueryExecutorResult | null;
|
||||
tradingMarginResult: AddressMcpQueryExecutorResult | null;
|
||||
receivablesResult: AddressMcpQueryExecutorResult | null;
|
||||
payablesResult: AddressMcpQueryExecutorResult | null;
|
||||
openContractsResult: AddressMcpQueryExecutorResult | null;
|
||||
|
|
@ -3171,6 +3330,9 @@ function summarizeBusinessOverviewRows(input: {
|
|||
if (input.taxResult && !input.taxResult.error) {
|
||||
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.tradingMarginResult && !input.tradingMarginResult.error) {
|
||||
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.receivablesResult && !input.receivablesResult.error) {
|
||||
parts.push(`${input.receivablesResult.fetched_rows} receivables rows fetched, ${input.receivablesResult.matched_rows} matched`);
|
||||
}
|
||||
|
|
@ -3228,6 +3390,13 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
|||
`НДС-позиция за ${derived.tax_position.period_scope} подтверждена по книгам продаж/покупок: продажи ${derived.tax_position.sales_vat_amount_human_ru}, покупки/вычеты ${derived.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${derived.tax_position.net_vat_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (derived.trading_margin_proxy) {
|
||||
const proxy = derived.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
facts.push(
|
||||
`Торговый margin proxy за ${proxy.period_scope} подтвержден по товарным документам продаж/поступлений: выручка продаж ${proxy.sales_revenue_human_ru}, закупочный документный след ${proxy.purchase_cost_proxy_human_ru}, валовый спред proxy ${proxy.gross_spread_proxy_human_ru}, маржинальность к выручке ${marginText}. Это не чистая прибыль и не бухгалтерский финрезультат.`
|
||||
);
|
||||
}
|
||||
if (derived.debt_position) {
|
||||
const debtDirection =
|
||||
derived.debt_position.net_debt_position_direction === "net_receivable"
|
||||
|
|
@ -3313,6 +3482,9 @@ function buildBusinessOverviewUnknownFacts(derived: AssistantMcpDiscoveryDerived
|
|||
missing.has("profit_margin")
|
||||
? "Прибыль и маржа этим бизнес-обзором не подтверждены: нужны себестоимость, расходы и закрывающие документы."
|
||||
: null,
|
||||
missing.has("accounting_profit_margin")
|
||||
? "Чистая прибыль, бухгалтерская маржа и финрезультат этим бизнес-обзором не подтверждены: торговый proxy показывает только документную разницу продаж и поступлений без расходов, закрытия периода и точной себестоимости продаж."
|
||||
: null,
|
||||
missing.has("debt_quality")
|
||||
? "Качество дебиторки/кредиторки этим бизнес-обзором не подтверждено: нужен отдельный долговой срез."
|
||||
: null,
|
||||
|
|
@ -4066,6 +4238,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
||||
let lifecycleResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let taxResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let tradingMarginResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let receivablesResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let payablesResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let openContractsResult: AddressMcpQueryExecutorResult | null = null;
|
||||
|
|
@ -4074,6 +4247,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const valueFilters = buildValueFlowFilters(planner);
|
||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
|
|
@ -4084,6 +4258,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const taxSelection = taxFilters
|
||||
? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||
: null;
|
||||
const tradingMarginSelection = tradingMarginFilters
|
||||
? selectAddressRecipe("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||
: null;
|
||||
const receivablesSelection = debtFilters
|
||||
? selectAddressRecipe("receivables_confirmed_as_of_date", debtFilters)
|
||||
: null;
|
||||
|
|
@ -4139,6 +4316,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
|
||||
}
|
||||
if (tradingMarginSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
|
||||
} else if (!tradingMarginFilters) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_probe_skipped_without_explicit_period");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview trading-margin proxy requires an executable explicit-period purchase/sale document recipe");
|
||||
}
|
||||
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_debt_recipes_selected");
|
||||
} else if (!debtFilters) {
|
||||
|
|
@ -4326,12 +4511,27 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
});
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, lifecycleResult));
|
||||
if (tradingMarginSelection?.selected_recipe) {
|
||||
const tradingMarginPlan = buildAddressRecipePlan(tradingMarginSelection.selected_recipe, tradingMarginFilters!);
|
||||
tradingMarginResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: tradingMarginPlan.query,
|
||||
limit: tradingMarginPlan.limit,
|
||||
account_scope: tradingMarginPlan.account_scope
|
||||
});
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, tradingMarginResult));
|
||||
}
|
||||
if (lifecycleResult.error) {
|
||||
pushUnique(queryLimitations, lifecycleResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_executed");
|
||||
}
|
||||
if (tradingMarginResult?.error) {
|
||||
pushUnique(queryLimitations, tradingMarginResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_query_mcp_error");
|
||||
} else if (tradingMarginResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_trading_margin_query_mcp_executed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -4344,6 +4544,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
|
|
@ -4365,6 +4566,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
if (derivedBusinessOverview.tax_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.debt_position) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_debt_position_from_confirmed_rows");
|
||||
}
|
||||
|
|
@ -4383,6 +4587,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export type AddressIntent =
|
|||
| "inventory_purchase_documents_for_item"
|
||||
| "inventory_supplier_stock_overlap_as_of_date"
|
||||
| "inventory_sale_trace_for_item"
|
||||
| "inventory_trading_margin_proxy_for_organization"
|
||||
| "inventory_profitability_for_item"
|
||||
| "inventory_purchase_to_sale_chain"
|
||||
| "inventory_aging_by_purchase_date"
|
||||
|
|
@ -196,6 +197,7 @@ export interface AddressRecipeDefinition {
|
|||
| "inventory_purchase_documents_profile"
|
||||
| "inventory_supplier_stock_overlap_profile"
|
||||
| "inventory_sale_trace_profile"
|
||||
| "inventory_trading_margin_proxy_profile"
|
||||
| "inventory_profitability_profile"
|
||||
| "inventory_purchase_to_sale_chain_profile"
|
||||
| "inventory_aging_by_purchase_date_profile";
|
||||
|
|
|
|||
|
|
@ -294,11 +294,22 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
{ Регистратор: "VAT_BOOK_PURCHASES", СчетДт: "19", Сумма: 12000 }
|
||||
]
|
||||
},
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
||||
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 200000, Item: "Товар А", Counterparty: "Клиент А", СчетКт: "41.01" },
|
||||
{ Period: "2020-02-01T00:00:00", Amount: 120000, Item: "Товар А", Counterparty: "Поставщик А", СчетДт: "41.01" }
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
|
@ -306,12 +317,20 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.headline).toContain("НДС-позиция");
|
||||
expect(draft.headline).toContain("торговый margin proxy");
|
||||
expect(draft.confirmed_lines.join("\n")).toContain("НДС-позиция за 2020");
|
||||
expect(draft.confirmed_lines.join("\n")).toContain("нетто к уплате 28 000 руб.");
|
||||
expect(draft.confirmed_lines.join("\n")).toContain("Торговый margin proxy за 2020");
|
||||
expect(draft.confirmed_lines.join("\n")).toContain("валовый спред proxy 80 000 руб.");
|
||||
expect(draft.confirmed_lines.join("\n")).toContain("не чистая прибыль");
|
||||
expect(draft.inference_lines.join("\n")).toContain("торговый спред proxy 80 000 руб.");
|
||||
expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа");
|
||||
expect(draft.unknown_lines.join("\n")).toContain("Чистая прибыль");
|
||||
expect(draft.unknown_lines.join("\n")).not.toContain("Налоговая/VAT-позиция");
|
||||
expect(draft.reason_codes).toContain("answer_contains_business_overview_tax_position");
|
||||
expect(draft.reason_codes).toContain("answer_contains_business_overview_trading_margin_proxy");
|
||||
expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin.");
|
||||
expect(draft.must_not_claim).toContain("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
|
||||
});
|
||||
|
||||
it("surfaces checked debt-position and open-settlement quality without treating them as overdue debt", async () => {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,12 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
||||
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 200000, Item: "Товар А", Counterparty: "Клиент А", СчетКт: "41.01" },
|
||||
{ Period: "2020-02-01T00:00:00", Amount: 120000, Item: "Товар А", Counterparty: "Поставщик А", СчетДт: "41.01" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
@ -251,6 +257,12 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
|
||||
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 200000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
|
||||
{ Period: "2020-02-01T00:00:00", Amount: 120000, Item: "Товар А", Counterparty: "Поставщик А", AccountDt: "41.01" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
@ -266,15 +278,96 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
net_vat_amount: 28000,
|
||||
net_vat_direction: "vat_to_pay"
|
||||
});
|
||||
expect(result.derived_business_overview?.trading_margin_proxy).toMatchObject({
|
||||
period_scope: "2020",
|
||||
sales_rows_with_amount: 1,
|
||||
purchase_rows_with_amount: 1,
|
||||
sales_revenue: 200000,
|
||||
purchase_cost_proxy: 120000,
|
||||
gross_spread_proxy: 80000,
|
||||
margin_to_revenue_pct: 40
|
||||
});
|
||||
expect(result.derived_business_overview?.missing_signal_families).not.toContain("tax_position");
|
||||
expect(result.derived_business_overview?.missing_signal_families).not.toContain("profit_margin");
|
||||
expect(result.derived_business_overview?.missing_signal_families).toContain("accounting_profit_margin");
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("НДС-позиция за 2020 подтверждена");
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("Торговый margin proxy за 2020");
|
||||
expect(result.evidence.unknown_facts.join("\n")).toContain("Чистая прибыль");
|
||||
expect(result.evidence.unknown_facts.join("\n")).not.toContain("Налоговая/VAT-позиция этим бизнес-обзором не подтверждена");
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_tax_query_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(9);
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_trading_margin_query_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(10);
|
||||
const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0];
|
||||
const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[9]?.[0];
|
||||
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж");
|
||||
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
|
||||
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары");
|
||||
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.ПоступлениеТоваровУслуг.Товары");
|
||||
});
|
||||
|
||||
it("keeps negative trading-margin proxy signed instead of reporting an absolute spread", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "broad_evaluation",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{ rows: [] },
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-03-01T00:00:00", Amount: 100000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
|
||||
{ Period: "2020-02-01T00:00:00", Amount: 150000, Item: "Товар А", Counterparty: "Поставщик А", AccountDt: "41.01" }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_business_overview?.trading_margin_proxy).toMatchObject({
|
||||
sales_revenue: 100000,
|
||||
purchase_cost_proxy: 150000,
|
||||
gross_spread_proxy: -50000,
|
||||
gross_spread_proxy_human_ru: "-50 000 руб.",
|
||||
margin_to_revenue_pct: -50
|
||||
});
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("валовый спред proxy -50 000 руб.");
|
||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("не чистая прибыль");
|
||||
});
|
||||
|
||||
it("adds a checked debt-position family to business overview only as an as-of-date snapshot", async () => {
|
||||
|
|
@ -337,7 +430,8 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" },
|
||||
{ Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ rows: [] }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
|
@ -393,7 +487,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.reason_codes).toContain("pilot_business_overview_open_contracts_query_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_age_signal_from_contract_dates");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(9);
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(10);
|
||||
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
|
||||
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
||||
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
|
||||
|
|
@ -458,7 +552,8 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" },
|
||||
{ Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ rows: [] }
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
|
@ -490,7 +585,7 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.evidence.unknown_facts.join("\n")).toContain("оборачиваемость");
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_inventory_query_mcp_executed");
|
||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(9);
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(10);
|
||||
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
|
||||
expect(inventoryCall?.account_scope).toContain("41.01");
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue