Open-World: добавить годовую динамику в бизнес-обзор

This commit is contained in:
dctouch 2026-05-04 11:47:31 +03:00
parent 027e9b373e
commit 04244ff3d7
9 changed files with 396 additions and 14 deletions

View File

@ -30,8 +30,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now
- Completed active slice: `Business Overview Gap-Specific Headline And Next-Step Precision`: broad company-analysis answers now name the remaining unchecked families from `missing_signal_families` instead of using stale generic profit/debt/VAT/warehouse wording after partial proxies are proven.
- Completed active slice: `Business Overview Debt Staleness Risk Proxy Bridge`: when current-turn open-settlement concentration and contract-date age are both present, business overview can include a bounded debt staleness-risk proxy while contractual delinquency, credit risk, and due-date aging remain unclaimed.
- Completed active slice: `Business Overview Supplier Concentration Proxy Bridge`: business overview now derives top suppliers/recipients from confirmed outgoing payment rows and surfaces procurement concentration without claiming vendor risk, procurement quality, or full expense structure.
- Completed active slice: `Business Overview Yearly Operating-Flow Proxy Bridge`: business overview now derives annual incoming/outgoing/net buckets from confirmed money-flow rows and can name the strongest incoming year and best operating-net year without claiming profit or P&L.
- Next active slice: continue breadth into exact company-wide accounting profit/margin, real due-date debt aging, confirmed inventory reserve/write-off/liquidation evidence, and broader unfamiliar 1C route families only where reviewed evidence routes exist.
- Active module progress: `~88% (Open-World Bounded Autonomy Breadth)`.
- Active module progress: `~90% (Open-World Bounded Autonomy Breadth)`.
## Reporting Rule
@ -68,7 +69,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, customer and supplier concentration, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, and warehouse staleness-risk proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, and confirmed reserve/write-off/liquidation inventory evidence families;
- extend `business_overview` beyond money-flow/activity, customer and supplier concentration, yearly operating-flow dynamics, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, and warehouse staleness-risk proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, and confirmed reserve/write-off/liquidation inventory 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;

View File

@ -504,3 +504,27 @@ Local validation is accepted for this slice:
- `npm.cmd run build`: passed.
Graphify rebuild after Slice 16 code/doc sync: `6041 nodes`, `13162 edges`, `136 communities`.
## Slice 17 - Business Overview Yearly Operating-Flow Proxy Bridge
This slice answers a common broad-analysis expectation without crossing into unsupported accounting profit.
User wording such as "какой самый доходный год" can mean profit, revenue, cash inflow, or operating scale. Until a reviewed P&L/profit route exists, the safe answer is to compute only what the current business-overview runtime actually proves: annual incoming payment flow, annual outgoing payment flow, and annual net over confirmed rows.
Implemented now:
- the pilot derives `yearly_breakdown` from the same confirmed incoming/outgoing money-flow rows already fetched by `business_overview`;
- each bucket contains year, incoming total, outgoing total, row counts, calculated net, human-readable amounts, and net direction;
- evidence and answer drafting can name the strongest year by confirmed incoming receipts and the best year by calculated operating net;
- headline/reason-code surfaces expose this as yearly operating-flow dynamics for semantic replay;
- answer boundaries explicitly say this is `operating-flow proxy`, not profit, финрезультат, or a complete annual P&L.
This improves management usefulness for broad company analysis while preserving the hard boundary that exact company-wide accounting profit still needs separately reviewed closing, cost, expense, and P&L evidence.
Local validation is accepted for this slice:
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts`: passed `66` with `1` skipped.
- `npm.cmd test -- addressQueryRuntimeM23.test.ts`: passed `412`.
- `npm.cmd run build`: passed.
Graphify rebuild after Slice 17 code/doc sync: `6047 nodes`, `13177 edges`, `139 communities`.

View File

@ -65,6 +65,7 @@ Status canon for planning:
- The current completed breadth slice is `Business Overview Gap-Specific Headline And Next-Step Precision`: business-overview answers now name remaining unchecked families from `missing_signal_families` instead of falling back to stale generic gap wording.
- The current completed breadth slice is `Business Overview Debt Staleness Risk Proxy Bridge`: when current-turn open-settlement concentration and contract-date age are both present, company analysis can include a bounded debt staleness-risk proxy while confirmed overdue debt, contractual delinquency, credit risk, and due-date aging remain unclaimed.
- The current completed breadth slice is `Business Overview Supplier Concentration Proxy Bridge`: company analysis now ranks confirmed outgoing payment counterparties and surfaces supplier/procurement concentration as a bounded proxy, not as vendor risk or full expense structure.
- The current completed breadth slice is `Business Overview Yearly Operating-Flow Proxy Bridge`: company analysis now builds annual incoming/outgoing/net buckets from confirmed money-flow rows and names strongest years as operating-flow proxy, not profit or full P&L.
- The next active breadth slice continues breadth into exact company-wide accounting profit/margin, real due-date debt aging, confirmed reserve/write-off/liquidation inventory evidence, 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).
@ -130,11 +131,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: `~88%`, 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, debt staleness-risk proxy bridged locally, supplier concentration proxy bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, and gap-specific answer shaping bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence are still pending
- active Open-World Bounded Autonomy Breadth progress: `~90%`, 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, debt staleness-risk proxy bridged locally, supplier concentration proxy bridged locally, yearly operating-flow proxy bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, and gap-specific answer shaping bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence 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: `6041 nodes`, `13162 edges`, `136 communities`
- graph snapshot after latest rebuild: `6047 nodes`, `13177 edges`, `139 communities`
- current regression-gate breakpoint:
- the validated hot paths are no longer structurally broken;
- flagship continuity collapse is no longer the primary risk;
@ -187,6 +188,7 @@ Latest live proof now includes:
- business-overview gap-specific answer shaping accepted locally: answer-adapter slice passed `34/34` with `1` skipped; build passed; graphify rebuilt to `6036 nodes`, `13149 edges`, `134 communities`; headline and next-step wording now follow `missing_signal_families` instead of stale generic gap labels
- business-overview debt staleness-risk proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6040 nodes`, `13158 edges`, `135 communities`; the proxy combines contract-date age and open-balance concentration while confirmed overdue debt, contractual delinquency, credit risk, and due-date aging remain unclaimed
- business-overview supplier concentration proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6041 nodes`, `13162 edges`, `136 communities`; the proxy ranks confirmed outgoing payment counterparties while vendor risk, procurement quality, and full expense structure remain unclaimed
- business-overview yearly operating-flow proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6047 nodes`, `13177 edges`, `139 communities`; the proxy builds annual incoming/outgoing/net buckets from confirmed money-flow rows while profit, финрезультат, and full P&L remain unclaimed
- inventory template lift accepted locally: catalog/data-need/planner/turn-input slice passed `139/139` with `6` skipped; full MCP-discovery slice passed `276/276` with `9` skipped; build passed; graphify stayed at `5912 nodes`, `12833 edges`, `138 communities`
- inventory runtime-boundary hardening accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `68/68` with `1` skipped; full MCP-discovery slice passed `277/277` with `9` skipped; build passed; graphify rebuilt to `5913 nodes`, `12837 edges`, `138 communities`
- inventory exact-runtime bridge accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `70/70` with `1` skipped; full MCP-discovery slice passed `279/279` with `9` skipped; build passed; graphify rebuilt to `5930 nodes`, `12884 edges`, `135 communities`

View File

@ -393,6 +393,9 @@ function headlineFor(mode, pilot) {
overview.outgoing_supplier_payout.rows_with_amount > 0) {
families.push("денежный поток");
}
if (overview.yearly_breakdown?.length) {
families.push("годовая operating-flow динамика");
}
if (overview.activity_period) {
families.push("активность");
}
@ -615,6 +618,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 yearly operating-flow breakdown as profit, financial result, or a complete annual P&L.");
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 present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
@ -899,6 +903,18 @@ function amountHumanRu(value) {
const rounded = Math.round(Math.abs(value) * 100) / 100;
return `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 }).format(rounded)} руб.`;
}
function yearCountHumanRu(count) {
const abs = Math.abs(count) % 100;
const last = abs % 10;
const noun = abs >= 11 && abs <= 14
? "лет"
: last === 1
? "год"
: last >= 2 && last <= 4
? "года"
: "лет";
return `${count} ${noun}`;
}
function percentOfTotal(part, total) {
if (!Number.isFinite(part) || !Number.isFinite(total) || total <= 0) {
return null;
@ -955,6 +971,9 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
if (supplierLeader) {
lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`);
}
if (overview.yearly_breakdown?.length) {
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`);
}
if (overview.activity_period) {
lines.push(`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date}${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`);
}
@ -1059,6 +1078,32 @@ function businessOverviewSupplierConcentrationLine(overview) {
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
}
function businessOverviewYearlyOperatingLine(overview) {
const years = overview.yearly_breakdown ?? [];
if (years.length === 0) {
return null;
}
const strongestIncomingYear = [...years]
.filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
const strongestNetYear = [...years]
.filter((bucket) => bucket.net_amount !== 0)
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
if (!strongestIncomingYear && !strongestNetYear) {
return null;
}
const parts = [];
if (strongestIncomingYear) {
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${strongestIncomingYear.incoming_total_amount_human_ru}`);
}
if (strongestNetYear) {
const netText = strongestNetYear.net_direction === "net_outgoing"
? `нетто исходящее ${strongestNetYear.net_amount_human_ru}`
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
}
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
}
function businessOverviewRiskSynthesisLine(overview) {
const signals = [];
if (overview.tax_position) {
@ -1144,6 +1189,7 @@ function derivedBusinessOverviewInferenceLines(pilot) {
businessOverviewCashSynthesisLine(overview),
businessOverviewCustomerConcentrationLine(overview),
businessOverviewSupplierConcentrationLine(overview),
businessOverviewYearlyOperatingLine(overview),
businessOverviewRiskSynthesisLine(overview),
businessOverviewExecutiveVerdictLine(overview),
"Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость."
@ -1202,6 +1248,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.derived_business_overview?.top_suppliers?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
}
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
}
if (pilot.derived_business_overview?.debt_position) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
}

View File

@ -1630,6 +1630,10 @@ function monthBucketFromIsoDate(isoDate) {
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
return match ? `${match[1]}-${match[2]}` : null;
}
function yearBucketFromIsoDate(isoDate) {
const match = isoDate?.match(/^(\d{4})-\d{2}-\d{2}$/);
return match ? match[1] : null;
}
function netDirectionFromAmount(amount) {
if (amount > 0) {
return "net_incoming";
@ -1698,6 +1702,18 @@ function formatAmountHumanRu(amount) {
.replace(/\u00a0/g, " ");
return `${formatted} руб.`;
}
function yearCountHumanRu(count) {
const abs = Math.abs(count) % 100;
const last = abs % 10;
const noun = abs >= 11 && abs <= 14
? "лет"
: last === 1
? "год"
: last >= 2 && last <= 4
? "года"
: "лет";
return `${count} ${noun}`;
}
function deriveValueFlowMonthBreakdown(result, aggregationAxis) {
if (!result || result.error || aggregationAxis !== "month") {
return [];
@ -1761,6 +1777,65 @@ function deriveBidirectionalValueFlowMonthBreakdown(input) {
};
});
}
function deriveBusinessOverviewSideYearBreakdown(result) {
if (!result || result.error) {
return [];
}
const buckets = new Map();
for (const row of result.rows) {
const yearBucket = yearBucketFromIsoDate(rowDateValue(row));
const amount = rowAmountValue(row);
if (!yearBucket || amount === null) {
continue;
}
const current = buckets.get(yearBucket) ?? { rows_with_amount: 0, total_amount: 0 };
current.rows_with_amount += 1;
current.total_amount += amount;
buckets.set(yearBucket, current);
}
return Array.from(buckets.entries())
.sort(([left], [right]) => left.localeCompare(right))
.map(([yearBucket, bucket]) => ({
year_bucket: yearBucket,
rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
}));
}
function deriveBusinessOverviewYearlyBreakdown(input) {
const incomingBuckets = deriveBusinessOverviewSideYearBreakdown(input.incomingResult);
const outgoingBuckets = deriveBusinessOverviewSideYearBreakdown(input.outgoingResult);
const allYearBuckets = new Set();
for (const bucket of incomingBuckets) {
allYearBuckets.add(bucket.year_bucket);
}
for (const bucket of outgoingBuckets) {
allYearBuckets.add(bucket.year_bucket);
}
const incomingByYear = new Map(incomingBuckets.map((bucket) => [bucket.year_bucket, bucket]));
const outgoingByYear = new Map(outgoingBuckets.map((bucket) => [bucket.year_bucket, bucket]));
return Array.from(allYearBuckets)
.sort((left, right) => left.localeCompare(right))
.map((yearBucket) => {
const incoming = incomingByYear.get(yearBucket);
const outgoing = outgoingByYear.get(yearBucket);
const incomingAmount = incoming?.total_amount ?? 0;
const outgoingAmount = outgoing?.total_amount ?? 0;
const netAmount = incomingAmount - outgoingAmount;
return {
year_bucket: yearBucket,
incoming_total_amount: incomingAmount,
incoming_total_amount_human_ru: formatAmountHumanRu(incomingAmount),
incoming_rows_with_amount: incoming?.rows_with_amount ?? 0,
outgoing_total_amount: outgoingAmount,
outgoing_total_amount_human_ru: formatAmountHumanRu(outgoingAmount),
outgoing_rows_with_amount: outgoing?.rows_with_amount ?? 0,
net_amount: netAmount,
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
net_direction: netDirectionFromAmount(netAmount)
};
});
}
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
if (!result || result.error || result.matched_rows <= 0) {
return null;
@ -2542,6 +2617,10 @@ function deriveBusinessOverview(input) {
direction: "outgoing_supplier_payout",
rankingNeed: "top_desc"
});
const yearlyBreakdown = deriveBusinessOverviewYearlyBreakdown({
incomingResult: input.incomingResult,
outgoingResult: input.outgoingResult
});
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
@ -2595,6 +2674,7 @@ function deriveBusinessOverview(input) {
net_direction: netDirectionFromAmount(netAmount),
top_customers: rankedIncoming?.ranked_values ?? [],
top_suppliers: rankedOutgoing?.ranked_values ?? [],
yearly_breakdown: yearlyBreakdown,
activity_period: activityPeriod,
tax_position: taxPosition,
trading_margin_proxy: tradingMarginProxy,
@ -2688,6 +2768,9 @@ function buildBusinessOverviewConfirmedFacts(derived) {
const leader = derived.top_suppliers[0];
facts.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`);
}
if (derived.yearly_breakdown.length > 0) {
facts.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`);
}
if (derived.activity_period) {
facts.push(`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date}${derived.activity_period.latest_activity_date}.`);
}
@ -2780,6 +2863,12 @@ function buildBusinessOverviewInferredFacts(derived) {
const supplierSharePct = supplierLeader
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
: null;
const strongestIncomingYear = [...derived.yearly_breakdown]
.filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
const strongestNetYear = [...derived.yearly_breakdown]
.filter((bucket) => bucket.net_amount !== 0)
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
return [
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
supplierLeader
@ -2787,6 +2876,12 @@ function buildBusinessOverviewInferredFacts(derived) {
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
: null,
strongestIncomingYear
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
: null,
strongestNetYear
? `Лучший год по расчетному операционному нетто найденных строк: ${strongestNetYear.year_bucket} (${netDirectionFromAmount(strongestNetYear.net_amount) === "net_outgoing" ? "нетто исходящее" : "нетто в плюс"} ${strongestNetYear.net_amount_human_ru}). Это не бухгалтерская прибыль.`
: null,
"Это операционный денежный сигнал по найденным строкам 1С, а не прибыль, маржа или бухгалтерское заключение о здоровье бизнеса."
].filter((fact) => Boolean(fact));
}
@ -3772,6 +3867,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (derivedBusinessOverview.top_suppliers.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
}
if (derivedBusinessOverview.yearly_breakdown.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
}
if (derivedBusinessOverview.activity_period) {
pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows");
}

View File

@ -492,6 +492,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
) {
families.push("денежный поток");
}
if (overview.yearly_breakdown?.length) {
families.push("годовая operating-flow динамика");
}
if (overview.activity_period) {
families.push("активность");
}
@ -724,6 +727,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 yearly operating-flow breakdown as profit, financial result, or a complete annual P&L.");
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 present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
@ -1043,6 +1047,20 @@ function amountHumanRu(value: number): string {
return `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 }).format(rounded)} руб.`;
}
function yearCountHumanRu(count: number): string {
const abs = Math.abs(count) % 100;
const last = abs % 10;
const noun =
abs >= 11 && abs <= 14
? "лет"
: last === 1
? "год"
: last >= 2 && last <= 4
? "года"
: "лет";
return `${count} ${noun}`;
}
function percentOfTotal(part: number, total: number): number | null {
if (!Number.isFinite(part) || !Number.isFinite(total) || total <= 0) {
return null;
@ -1113,6 +1131,11 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
);
}
if (overview.yearly_breakdown?.length) {
lines.push(
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
);
}
if (overview.activity_period) {
lines.push(
`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date}${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`
@ -1245,6 +1268,33 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
}
function businessOverviewYearlyOperatingLine(overview: BusinessOverview): string | null {
const years = overview.yearly_breakdown ?? [];
if (years.length === 0) {
return null;
}
const strongestIncomingYear = [...years]
.filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
const strongestNetYear = [...years]
.filter((bucket) => bucket.net_amount !== 0)
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
if (!strongestIncomingYear && !strongestNetYear) {
return null;
}
const parts: string[] = [];
if (strongestIncomingYear) {
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${strongestIncomingYear.incoming_total_amount_human_ru}`);
}
if (strongestNetYear) {
const netText = strongestNetYear.net_direction === "net_outgoing"
? `нетто исходящее ${strongestNetYear.net_amount_human_ru}`
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
}
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
}
function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | null {
const signals: string[] = [];
if (overview.tax_position) {
@ -1341,6 +1391,7 @@ function derivedBusinessOverviewInferenceLines(pilot: AssistantMcpDiscoveryPilot
businessOverviewCashSynthesisLine(overview),
businessOverviewCustomerConcentrationLine(overview),
businessOverviewSupplierConcentrationLine(overview),
businessOverviewYearlyOperatingLine(overview),
businessOverviewRiskSynthesisLine(overview),
businessOverviewExecutiveVerdictLine(overview),
"Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость."
@ -1406,6 +1457,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.derived_business_overview?.top_suppliers?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
}
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
}
if (pilot.derived_business_overview?.debt_position) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
}

View File

@ -125,6 +125,19 @@ export interface AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket {
net_direction: AssistantMcpDiscoveryNetDirection;
}
export interface AssistantMcpDiscoveryBusinessOverviewYearBucket {
year_bucket: string;
incoming_total_amount: number;
incoming_total_amount_human_ru: string;
incoming_rows_with_amount: number;
outgoing_total_amount: number;
outgoing_total_amount_human_ru: string;
outgoing_rows_with_amount: number;
net_amount: number;
net_amount_human_ru: string;
net_direction: AssistantMcpDiscoveryNetDirection;
}
export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
counterparty: string | null;
period_scope: string | null;
@ -151,6 +164,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
net_direction: AssistantMcpDiscoveryNetDirection;
top_customers: AssistantMcpDiscoveryRankedValueFlowBucket[];
top_suppliers: AssistantMcpDiscoveryRankedValueFlowBucket[];
yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[];
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
@ -2352,6 +2366,11 @@ function monthBucketFromIsoDate(isoDate: string | null): string | null {
return match ? `${match[1]}-${match[2]}` : null;
}
function yearBucketFromIsoDate(isoDate: string | null): string | null {
const match = isoDate?.match(/^(\d{4})-\d{2}-\d{2}$/);
return match ? match[1] : null;
}
function netDirectionFromAmount(amount: number): AssistantMcpDiscoveryNetDirection {
if (amount > 0) {
return "net_incoming";
@ -2427,6 +2446,20 @@ function formatAmountHumanRu(amount: number): string {
return `${formatted} руб.`;
}
function yearCountHumanRu(count: number): string {
const abs = Math.abs(count) % 100;
const last = abs % 10;
const noun =
abs >= 11 && abs <= 14
? "лет"
: last === 1
? "год"
: last >= 2 && last <= 4
? "года"
: "лет";
return `${count} ${noun}`;
}
function deriveValueFlowMonthBreakdown(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
@ -2502,6 +2535,74 @@ function deriveBidirectionalValueFlowMonthBreakdown(input: {
});
}
function deriveBusinessOverviewSideYearBreakdown(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
): Array<{ year_bucket: string; rows_with_amount: number; total_amount: number; total_amount_human_ru: string }> {
if (!result || result.error) {
return [];
}
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
for (const row of result.rows) {
const yearBucket = yearBucketFromIsoDate(rowDateValue(row));
const amount = rowAmountValue(row);
if (!yearBucket || amount === null) {
continue;
}
const current = buckets.get(yearBucket) ?? { rows_with_amount: 0, total_amount: 0 };
current.rows_with_amount += 1;
current.total_amount += amount;
buckets.set(yearBucket, current);
}
return Array.from(buckets.entries())
.sort(([left], [right]) => left.localeCompare(right))
.map(([yearBucket, bucket]) => ({
year_bucket: yearBucket,
rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
}));
}
function deriveBusinessOverviewYearlyBreakdown(input: {
incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
}): AssistantMcpDiscoveryBusinessOverviewYearBucket[] {
const incomingBuckets = deriveBusinessOverviewSideYearBreakdown(input.incomingResult);
const outgoingBuckets = deriveBusinessOverviewSideYearBreakdown(input.outgoingResult);
const allYearBuckets = new Set<string>();
for (const bucket of incomingBuckets) {
allYearBuckets.add(bucket.year_bucket);
}
for (const bucket of outgoingBuckets) {
allYearBuckets.add(bucket.year_bucket);
}
const incomingByYear = new Map(incomingBuckets.map((bucket) => [bucket.year_bucket, bucket]));
const outgoingByYear = new Map(outgoingBuckets.map((bucket) => [bucket.year_bucket, bucket]));
return Array.from(allYearBuckets)
.sort((left, right) => left.localeCompare(right))
.map((yearBucket) => {
const incoming = incomingByYear.get(yearBucket);
const outgoing = outgoingByYear.get(yearBucket);
const incomingAmount = incoming?.total_amount ?? 0;
const outgoingAmount = outgoing?.total_amount ?? 0;
const netAmount = incomingAmount - outgoingAmount;
return {
year_bucket: yearBucket,
incoming_total_amount: incomingAmount,
incoming_total_amount_human_ru: formatAmountHumanRu(incomingAmount),
incoming_rows_with_amount: incoming?.rows_with_amount ?? 0,
outgoing_total_amount: outgoingAmount,
outgoing_total_amount_human_ru: formatAmountHumanRu(outgoingAmount),
outgoing_rows_with_amount: outgoing?.rows_with_amount ?? 0,
net_amount: netAmount,
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
net_direction: netDirectionFromAmount(netAmount)
};
});
}
function deriveValueFlow(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
counterparty: string | null,
@ -3441,6 +3542,10 @@ function deriveBusinessOverview(input: {
direction: "outgoing_supplier_payout",
rankingNeed: "top_desc"
});
const yearlyBreakdown = deriveBusinessOverviewYearlyBreakdown({
incomingResult: input.incomingResult,
outgoingResult: input.outgoingResult
});
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
@ -3495,6 +3600,7 @@ function deriveBusinessOverview(input: {
net_direction: netDirectionFromAmount(netAmount),
top_customers: rankedIncoming?.ranked_values ?? [],
top_suppliers: rankedOutgoing?.ranked_values ?? [],
yearly_breakdown: yearlyBreakdown,
activity_period: activityPeriod,
tax_position: taxPosition,
trading_margin_proxy: tradingMarginProxy,
@ -3611,6 +3717,11 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`
);
}
if (derived.yearly_breakdown.length > 0) {
facts.push(
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`
);
}
if (derived.activity_period) {
facts.push(
`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date}${derived.activity_period.latest_activity_date}.`
@ -3731,6 +3842,12 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
const supplierSharePct = supplierLeader
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
: null;
const strongestIncomingYear = [...derived.yearly_breakdown]
.filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
const strongestNetYear = [...derived.yearly_breakdown]
.filter((bucket) => bucket.net_amount !== 0)
.sort((left, right) => right.net_amount - left.net_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
return [
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
supplierLeader
@ -3738,6 +3855,12 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
: null,
strongestIncomingYear
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
: null,
strongestNetYear
? `Лучший год по расчетному операционному нетто найденных строк: ${strongestNetYear.year_bucket} (${netDirectionFromAmount(strongestNetYear.net_amount) === "net_outgoing" ? "нетто исходящее" : "нетто в плюс"} ${strongestNetYear.net_amount_human_ru}). Это не бухгалтерская прибыль.`
: null,
"Это операционный денежный сигнал по найденным строкам 1С, а не прибыль, маржа или бухгалтерское заключение о здоровье бизнеса."
].filter((fact): fact is string => Boolean(fact));
}
@ -4839,6 +4962,9 @@ export async function executeAssistantMcpDiscoveryPilot(
if (derivedBusinessOverview.top_suppliers.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
}
if (derivedBusinessOverview.yearly_breakdown.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
}
if (derivedBusinessOverview.activity_period) {
pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows");
}

View File

@ -218,11 +218,15 @@ describe("assistant MCP discovery answer adapter", () => {
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" },
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" }
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" },
{ Period: "2021-03-15T00:00:00", Amount: 220000, Counterparty: "Клиент А" }
]
},
{
rows: [{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" }]
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" },
{ Period: "2021-03-20T00:00:00", Amount: 50000, Counterparty: "Поставщик Б" }
]
},
{
rows: [
@ -240,17 +244,22 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.confirmed_lines.join("\n")).toContain("Входящие поступления");
expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный клиент");
expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный поставщик");
expect(draft.confirmed_lines.join("\n")).toContain("Годовая раскладка операционного денежного потока");
expect(draft.inference_lines.join("\n")).toContain("Аналитический вывод по оборотам");
expect(draft.inference_lines.join("\n")).toContain("Концентрация входящего потока");
expect(draft.inference_lines.join("\n")).toContain("Концентрация исходящего потока");
expect(draft.inference_lines.join("\n")).toContain("Годовая динамика по проверенным строкам");
expect(draft.inference_lines.join("\n")).toContain("2021");
expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит");
expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа");
expect(draft.unknown_lines.join("\n")).toContain("Прибыль и маржа");
expect(draft.unknown_lines.join("\n")).toContain("Налоговая/VAT-позиция");
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 yearly operating-flow breakdown as profit, financial result, or a complete annual P&L.");
expect(draft.must_not_claim).toContain("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
expect(draft.reason_codes).toContain("answer_contains_business_overview");
expect(draft.reason_codes).toContain("answer_contains_business_overview_supplier_concentration");
expect(draft.reason_codes).toContain("answer_contains_business_overview_yearly_operating_breakdown");
expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis");
});

View File

@ -138,12 +138,14 @@ describe("assistant MCP discovery pilot executor", () => {
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" },
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" }
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" },
{ Period: "2021-03-15T00:00:00", Amount: 220000, Counterparty: "Клиент А" }
]
},
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" }
{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" },
{ Period: "2021-03-20T00:00:00", Amount: 50000, Counterparty: "Поставщик Б" }
]
},
{
@ -175,33 +177,50 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.derived_business_overview).toMatchObject({
organization_scope: "ООО Альтернатива Плюс",
incoming_customer_revenue: {
total_amount: 420000,
rows_with_amount: 3
},
outgoing_supplier_payout: {
total_amount: 200000,
rows_with_amount: 2
},
outgoing_supplier_payout: {
total_amount: 150000,
rows_with_amount: 1
},
net_amount: 50000,
net_amount: 220000,
net_direction: "net_incoming"
});
expect(result.derived_business_overview?.top_customers[0]).toMatchObject({
axis_value: "Клиент А",
total_amount: 120000
total_amount: 340000
});
expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({
axis_value: "Поставщик А",
total_amount: 150000
});
expect(result.derived_business_overview?.yearly_breakdown).toMatchObject([
{
year_bucket: "2020",
incoming_total_amount: 200000,
outgoing_total_amount: 150000,
net_amount: 50000
},
{
year_bucket: "2021",
incoming_total_amount: 220000,
outgoing_total_amount: 50000,
net_amount: 170000
}
]);
expect(result.derived_business_overview?.activity_period?.duration_total_months).toBe(11);
expect(result.evidence.confirmed_facts.join("\n")).toContain("В 1С подтверждены входящие поступления");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Самый крупный подтвержденный поставщик");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Годовая раскладка операционного денежного потока");
expect(result.evidence.inferred_facts.join("\n")).toContain("procurement concentration proxy");
expect(result.evidence.inferred_facts.join("\n")).toContain("Самый сильный год по подтвержденным входящим поступлениям: 2021");
expect(result.evidence.unknown_facts).toContain(
"Прибыль и маржа этим бизнес-обзором не подтверждены: нужны себестоимость, расходы и закрывающие документы."
);
expect(result.reason_codes).toContain("pilot_derived_business_overview_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(3);
});