Open-World: добавить возрастной сигнал открытых расчетов
This commit is contained in:
parent
7294eca381
commit
9b26bd05ef
|
|
@ -22,8 +22,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now
|
||||||
- Completed active slice: `Business Overview Inventory-Position Fact-Family Bridge`: explicit-date business overview can include confirmed stock-on-hand inventory position, while all-time follow-ups do not reuse stale inventory snapshots and inventory liquidity/turnover remains unclaimed.
|
- Completed active slice: `Business Overview Inventory-Position Fact-Family Bridge`: explicit-date business overview can include confirmed stock-on-hand inventory position, while all-time follow-ups do not reuse stale inventory snapshots and inventory liquidity/turnover remains unclaimed.
|
||||||
- Completed active slice: `Business Overview Open-Settlement Quality Bridge`: explicit-period business overview can check open-contract settlement concentration on 60/62/76, while due-date aging/overdue debt remains unclaimed until a reviewed due-date route exists.
|
- Completed active slice: `Business Overview Open-Settlement Quality Bridge`: explicit-period business overview can check open-contract settlement concentration on 60/62/76, while due-date aging/overdue debt remains unclaimed until a reviewed due-date route exists.
|
||||||
- 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: `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.
|
||||||
- Next active slice: continue breadth into company-wide profit/margin and due-date debt aging only where reviewed evidence routes exist.
|
- 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.
|
||||||
- Active module progress: `~66% (Open-World Bounded Autonomy Breadth)`.
|
- 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: `~70% (Open-World Bounded Autonomy Breadth)`.
|
||||||
|
|
||||||
## Reporting Rule
|
## Reporting Rule
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ The project is not yet a universal arbitrary-1C agent.
|
||||||
|
|
||||||
Remaining work belongs to the next breadth module:
|
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, 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, and as-of-date inventory position into separately proven company-wide profit/margin, due-date debt aging/overdue, and real inventory-liquidity evidence families;
|
||||||
- broader dynamic schema traversal for unfamiliar 1C asks;
|
- broader dynamic schema traversal for unfamiliar 1C asks;
|
||||||
- more primitive descriptors where live evidence proves a real gap;
|
- 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;
|
- more replay-backed domain packs that start from user business meaning, not from route convenience;
|
||||||
|
|
|
||||||
|
|
@ -211,13 +211,29 @@ Local validation is accepted for this slice:
|
||||||
- `npm.cmd test -- addressQueryRuntimeM23.test.ts`: passed `412/412`;
|
- `npm.cmd test -- addressQueryRuntimeM23.test.ts`: passed `412/412`;
|
||||||
- `npm.cmd run build`: passed.
|
- `npm.cmd run build`: passed.
|
||||||
|
|
||||||
|
## Slice 9 - Business Overview Contract-Date Debt Age Signal Bridge
|
||||||
|
|
||||||
|
This slice deepens the open-settlement quality family without crossing the evidence boundary into contractual overdue debt.
|
||||||
|
|
||||||
|
Implemented now:
|
||||||
|
|
||||||
|
- `debt_open_settlement_quality` now carries an optional `age_signal` derived only from dates found inside confirmed open-contract rows;
|
||||||
|
- the pilot extracts contract-like dates from contract/document/analytics text, normalizes them to ISO dates, and computes a max age in days against the explicit as-of date;
|
||||||
|
- the derived signal reports oldest/latest contract date, contracts with known start dates, and top aged contracts by age and amount;
|
||||||
|
- the answer adapter surfaces this as `возрастной сигнал открытых расчетов`, not as payment-term delinquency, credit risk, or confirmed overdue debt;
|
||||||
|
- `debt_due_date_aging_quality` remains a missing family, because dates embedded in contract names prove contract age, not contractual payment due dates.
|
||||||
|
|
||||||
|
Local validation is accepted for this slice:
|
||||||
|
|
||||||
|
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts`: passed `65/65` with `1` skipped.
|
||||||
|
|
||||||
### Still Pending Breadth Slices
|
### Still Pending Breadth Slices
|
||||||
|
|
||||||
Grow this bridge beyond the first confirmed signal bundle:
|
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 routes exist;
|
- add separate evidence families for company-wide profit/margin and due-date debt aging/overdue quality where reviewed 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;
|
- extend inventory evidence from as-of-date stock position into real turnover/liquidity only when reviewed sales velocity, aging, or obsolescence evidence exists;
|
||||||
- extend debt evidence from as-of-date position/open-settlement concentration into overdue aging only when reviewed due-date or aging 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;
|
- extend VAT/tax beyond explicit-period tax position only when the requested tax fact is provable and the period is explicit;
|
||||||
- keep Post-F stale-scope and phase83 catalog-alignment canaries green while widening the route.
|
- keep Post-F stale-scope and phase83 catalog-alignment canaries green while widening the route.
|
||||||
|
|
||||||
|
|
@ -287,3 +303,11 @@ Selected-item profitability route validation:
|
||||||
- `npm.cmd run build`: passed.
|
- `npm.cmd run build`: passed.
|
||||||
|
|
||||||
Graphify rebuild after Slice 8 code/doc sync: `6012 nodes`, `13086 edges`, `138 communities`.
|
Graphify rebuild after Slice 8 code/doc sync: `6012 nodes`, `13086 edges`, `138 communities`.
|
||||||
|
|
||||||
|
Contract-date debt age signal validation:
|
||||||
|
|
||||||
|
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts`: passed `65/65` with `1` skipped.
|
||||||
|
- `npm.cmd test -- assistantMcp`: passed `305/305` with `9` skipped.
|
||||||
|
- `npm.cmd run build`: passed.
|
||||||
|
|
||||||
|
Graphify rebuild after Slice 9 code/doc sync: `6016 nodes`, `13098 edges`, `139 communities`.
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ Status canon for planning:
|
||||||
- The current completed breadth slice is `Business Overview Inventory-Position Fact-Family Bridge`: explicit-date business overview can include confirmed stock-on-hand inventory position, while all-time follow-ups do not reuse stale inventory snapshots and inventory liquidity/turnover remains unclaimed.
|
- The current completed breadth slice is `Business Overview Inventory-Position Fact-Family Bridge`: explicit-date business overview can include confirmed stock-on-hand inventory position, while all-time follow-ups do not reuse stale inventory snapshots and inventory liquidity/turnover remains unclaimed.
|
||||||
- The current completed breadth slice is `Business Overview Open-Settlement Quality Bridge`: explicit-period business overview can check open-contract settlement concentration, while due-date aging and confirmed overdue debt remain outside the answer until a reviewed due-date route exists.
|
- The current completed breadth slice is `Business Overview Open-Settlement Quality Bridge`: explicit-period business overview can check open-contract settlement concentration, while due-date aging and confirmed overdue debt remain outside the answer until a reviewed due-date route exists.
|
||||||
- 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 `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 next active breadth slice continues breadth into company-wide profit/margin and due-date debt aging, then broader unfamiliar 1C route breadth without relaxing truth boundaries.
|
- 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 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 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).
|
- 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:
|
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:
|
||||||
|
|
@ -122,11 +123,11 @@ Current honest status:
|
||||||
- pre-multidomain readiness: `~90%`
|
- pre-multidomain readiness: `~90%`
|
||||||
- bounded-autonomy foundation readiness: `~89%`
|
- bounded-autonomy foundation readiness: `~89%`
|
||||||
- open-world bounded-autonomy readiness: `~87%`
|
- open-world bounded-autonomy readiness: `~87%`
|
||||||
- active Open-World Bounded Autonomy Breadth progress: `~66%`, 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, and selected-item profitability bridged by local semantic/runtime regression tests; company-wide profit/margin, due-date debt aging/overdue, and real inventory-liquidity expansion are still pending
|
- active Open-World Bounded Autonomy Breadth progress: `~70%`, 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, and contract-date debt age bridged locally; company-wide 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
|
- 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
|
- 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
|
- 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: `6012 nodes`, `13086 edges`, `138 communities`
|
- graph snapshot after latest rebuild: `6016 nodes`, `13098 edges`, `139 communities`
|
||||||
- current regression-gate breakpoint:
|
- current regression-gate breakpoint:
|
||||||
- the validated hot paths are no longer structurally broken;
|
- the validated hot paths are no longer structurally broken;
|
||||||
- flagship continuity collapse is no longer the primary risk;
|
- flagship continuity collapse is no longer the primary risk;
|
||||||
|
|
@ -173,6 +174,7 @@ Latest live proof now includes:
|
||||||
- business-overview debt-position fact-family bridge accepted live: `address_truth_harness_phase86_business_overview_debt_position_live_20260504_debt2` accepted `2/2`, proving explicit-period receivables/payables as-of-date debt position and all-time follow-up protection against stale debt snapshot reuse
|
- business-overview debt-position fact-family bridge accepted live: `address_truth_harness_phase86_business_overview_debt_position_live_20260504_debt2` accepted `2/2`, proving explicit-period receivables/payables as-of-date debt position and all-time follow-up protection against stale debt snapshot reuse
|
||||||
- business-overview inventory-position fact-family bridge accepted live: `address_truth_harness_phase87_business_overview_inventory_position_live_20260504_inventory2` accepted `2/2`, proving explicit-date stock-on-hand position and all-time follow-up protection against stale inventory snapshot reuse
|
- business-overview inventory-position fact-family bridge accepted live: `address_truth_harness_phase87_business_overview_inventory_position_live_20260504_inventory2` accepted `2/2`, proving explicit-date stock-on-hand position and all-time follow-up protection against stale inventory snapshot reuse
|
||||||
- business-overview open-settlement quality bridge accepted live: `address_truth_harness_phase88_business_overview_open_settlement_quality_live_20260504_openquality4` accepted `2/2`, proving explicit-period open-contract concentration and all-time follow-up protection against stale open-contract/debt-quality reuse
|
- business-overview open-settlement quality bridge accepted live: `address_truth_harness_phase88_business_overview_open_settlement_quality_live_20260504_openquality4` accepted `2/2`, proving explicit-period open-contract concentration and all-time follow-up protection against stale open-contract/debt-quality reuse
|
||||||
|
- business-overview contract-date debt age signal accepted locally: targeted executor/answer-adapter slice passed `65/65` with `1` skipped; full MCP-discovery slice passed `305/305` with `9` skipped; build passed; graphify rebuilt to `6016 nodes`, `13098 edges`, `139 communities`; contract-date age is surfaced as a bounded signal while due-date aging/overdue debt remains 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 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 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`
|
- 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`
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,9 @@ function headlineFor(mode, pilot) {
|
||||||
}
|
}
|
||||||
if (overview.debt_open_settlement_quality) {
|
if (overview.debt_open_settlement_quality) {
|
||||||
families.push("качество открытых расчетов");
|
families.push("качество открытых расчетов");
|
||||||
|
if (overview.debt_open_settlement_quality.age_signal) {
|
||||||
|
families.push("возрастной сигнал открытых расчетов");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (overview.inventory_position) {
|
if (overview.inventory_position) {
|
||||||
families.push("складской срез на дату");
|
families.push("складской срез на дату");
|
||||||
|
|
@ -863,6 +866,12 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
||||||
? ` Крупнейший открытый договор: ${topContract.contract}${topContract.counterparty ? ` / ${topContract.counterparty}` : ""} — ${topContract.total_amount_human_ru}${topContract.share_of_gross_open_amount_pct === null ? "" : ` (${topContract.share_of_gross_open_amount_pct}%)`}.`
|
? ` Крупнейший открытый договор: ${topContract.contract}${topContract.counterparty ? ` / ${topContract.counterparty}` : ""} — ${topContract.total_amount_human_ru}${topContract.share_of_gross_open_amount_pct === null ? "" : ` (${topContract.share_of_gross_open_amount_pct}%)`}.`
|
||||||
: "";
|
: "";
|
||||||
lines.push(`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`);
|
lines.push(`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`);
|
||||||
|
if (quality.age_signal?.oldest_start_date) {
|
||||||
|
const ageText = quality.age_signal.max_age_days === null
|
||||||
|
? ""
|
||||||
|
: `, максимальный возраст сигнала ${quality.age_signal.max_age_days} дн.`;
|
||||||
|
lines.push(`Возрастной сигнал открытых расчетов: самая ранняя найденная дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не просрочка и не due-date анализ.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (overview.inventory_position) {
|
if (overview.inventory_position) {
|
||||||
const leader = overview.inventory_position.top_items[0];
|
const leader = overview.inventory_position.top_items[0];
|
||||||
|
|
@ -940,6 +949,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
}
|
}
|
||||||
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
|
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
|
||||||
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
|
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
|
||||||
|
if (pilot.derived_business_overview.debt_open_settlement_quality.age_signal) {
|
||||||
|
pushReason(reasonCodes, "answer_contains_business_overview_debt_age_signal");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pilot.derived_business_overview?.inventory_position) {
|
if (pilot.derived_business_overview?.inventory_position) {
|
||||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||||
|
|
|
||||||
|
|
@ -1541,6 +1541,66 @@ function rowCounterpartyValue(row) {
|
||||||
}
|
}
|
||||||
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
||||||
}
|
}
|
||||||
|
function normalizeDateParts(yearText, monthText, dayText) {
|
||||||
|
const year = Number(yearText);
|
||||||
|
const month = Number(monthText);
|
||||||
|
const day = Number(dayText);
|
||||||
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (date.getUTCFullYear() !== year ||
|
||||||
|
date.getUTCMonth() !== month - 1 ||
|
||||||
|
date.getUTCDate() !== day) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
String(year).padStart(4, "0"),
|
||||||
|
String(month).padStart(2, "0"),
|
||||||
|
String(day).padStart(2, "0")
|
||||||
|
].join("-");
|
||||||
|
}
|
||||||
|
function extractContractDateFromText(value) {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
|
||||||
|
if (isoLikeMatch) {
|
||||||
|
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||||
|
}
|
||||||
|
const ruDateMatch = text.match(/(\d{1,2})[./-](\d{1,2})[./-](\d{4})/);
|
||||||
|
if (ruDateMatch) {
|
||||||
|
return normalizeDateParts(ruDateMatch[3], ruDateMatch[2], ruDateMatch[1]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function earlierIsoDate(left, right) {
|
||||||
|
if (!left) {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
if (!right) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
return right < left ? right : left;
|
||||||
|
}
|
||||||
|
function rowOpenSettlementContractStartDateValue(row) {
|
||||||
|
const candidates = [
|
||||||
|
rowContractValue(row),
|
||||||
|
rowDocumentValue(row),
|
||||||
|
...rowAnalyticsTextValues(row)
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const contractDate = extractContractDateFromText(candidate);
|
||||||
|
if (contractDate) {
|
||||||
|
return contractDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function monthBucketFromIsoDate(isoDate) {
|
function monthBucketFromIsoDate(isoDate) {
|
||||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||||
return match ? `${match[1]}-${match[2]}` : null;
|
return match ? `${match[1]}-${match[2]}` : null;
|
||||||
|
|
@ -1982,6 +2042,7 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input) {
|
||||||
if (!input.debtAsOfDate || !input.openContractsResult || input.openContractsResult.error || input.openContractsResult.matched_rows <= 0) {
|
if (!input.debtAsOfDate || !input.openContractsResult || input.openContractsResult.error || input.openContractsResult.matched_rows <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const debtAsOfDate = input.debtAsOfDate;
|
||||||
const counterpartyBuckets = new Map();
|
const counterpartyBuckets = new Map();
|
||||||
const contractBuckets = new Map();
|
const contractBuckets = new Map();
|
||||||
const counterparties = new Set();
|
const counterparties = new Set();
|
||||||
|
|
@ -2014,15 +2075,18 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input) {
|
||||||
rowsWithoutCounterparty += 1;
|
rowsWithoutCounterparty += 1;
|
||||||
}
|
}
|
||||||
if (contract) {
|
if (contract) {
|
||||||
|
const contractStartDate = rowOpenSettlementContractStartDateValue(row);
|
||||||
contracts.add(contract);
|
contracts.add(contract);
|
||||||
const current = contractBuckets.get(contract) ?? {
|
const current = contractBuckets.get(contract) ?? {
|
||||||
counterparty,
|
counterparty,
|
||||||
|
contract_start_date: contractStartDate,
|
||||||
rows_with_amount: 0,
|
rows_with_amount: 0,
|
||||||
total_amount: 0
|
total_amount: 0
|
||||||
};
|
};
|
||||||
if (!current.counterparty && counterparty) {
|
if (!current.counterparty && counterparty) {
|
||||||
current.counterparty = counterparty;
|
current.counterparty = counterparty;
|
||||||
}
|
}
|
||||||
|
current.contract_start_date = earlierIsoDate(current.contract_start_date, contractStartDate);
|
||||||
current.rows_with_amount += 1;
|
current.rows_with_amount += 1;
|
||||||
current.total_amount += absAmount;
|
current.total_amount += absAmount;
|
||||||
contractBuckets.set(contract, current);
|
contractBuckets.set(contract, current);
|
||||||
|
|
@ -2050,6 +2114,7 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input) {
|
||||||
.map(([contract, bucket]) => ({
|
.map(([contract, bucket]) => ({
|
||||||
contract,
|
contract,
|
||||||
counterparty: bucket.counterparty,
|
counterparty: bucket.counterparty,
|
||||||
|
contract_start_date: bucket.contract_start_date,
|
||||||
rows_with_amount: bucket.rows_with_amount,
|
rows_with_amount: bucket.rows_with_amount,
|
||||||
total_amount: bucket.total_amount,
|
total_amount: bucket.total_amount,
|
||||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||||
|
|
@ -2060,8 +2125,51 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input) {
|
||||||
return amountDelta !== 0 ? amountDelta : left.contract.localeCompare(right.contract, "ru");
|
return amountDelta !== 0 ? amountDelta : left.contract.localeCompare(right.contract, "ru");
|
||||||
})
|
})
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
|
const agedContractBuckets = Array.from(contractBuckets.entries())
|
||||||
|
.map(([contract, bucket]) => {
|
||||||
|
if (!bucket.contract_start_date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contract,
|
||||||
|
counterparty: bucket.counterparty,
|
||||||
|
start_date: bucket.contract_start_date,
|
||||||
|
age_days: daysBetweenIsoDates(bucket.contract_start_date, debtAsOfDate),
|
||||||
|
total_amount: bucket.total_amount,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||||
|
share_of_gross_open_amount_pct: percentageOfTotal(bucket.total_amount, grossOpenAmount)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => Boolean(item))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftAge = left.age_days ?? -1;
|
||||||
|
const rightAge = right.age_days ?? -1;
|
||||||
|
const ageDelta = rightAge - leftAge;
|
||||||
|
if (ageDelta !== 0) {
|
||||||
|
return ageDelta;
|
||||||
|
}
|
||||||
|
const amountDelta = right.total_amount - left.total_amount;
|
||||||
|
return amountDelta !== 0 ? amountDelta : left.contract.localeCompare(right.contract, "ru");
|
||||||
|
});
|
||||||
|
const debtAgeDates = agedContractBuckets.map((bucket) => bucket.start_date).sort();
|
||||||
|
const maxAgeDays = agedContractBuckets.reduce((max, bucket) => {
|
||||||
|
if (bucket.age_days === null) {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
return max === null ? bucket.age_days : Math.max(max, bucket.age_days);
|
||||||
|
}, null);
|
||||||
|
const ageSignal = agedContractBuckets.length > 0
|
||||||
|
? {
|
||||||
|
contracts_with_start_date: agedContractBuckets.length,
|
||||||
|
oldest_start_date: debtAgeDates[0] ?? null,
|
||||||
|
latest_start_date: debtAgeDates[debtAgeDates.length - 1] ?? null,
|
||||||
|
max_age_days: maxAgeDays,
|
||||||
|
top_aged_contracts: agedContractBuckets.slice(0, 5),
|
||||||
|
inference_basis: "contract_dates_from_open_settlement_rows"
|
||||||
|
}
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
as_of_date: input.debtAsOfDate,
|
as_of_date: debtAsOfDate,
|
||||||
rows_matched: input.openContractsResult.matched_rows,
|
rows_matched: input.openContractsResult.matched_rows,
|
||||||
rows_with_amount: rowsWithAmount,
|
rows_with_amount: rowsWithAmount,
|
||||||
gross_open_amount: grossOpenAmount,
|
gross_open_amount: grossOpenAmount,
|
||||||
|
|
@ -2074,6 +2182,7 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input) {
|
||||||
top_contracts: topContracts,
|
top_contracts: topContracts,
|
||||||
concentration_top_counterparty_pct: percentageOfTotal(topCounterparties[0]?.total_amount ?? 0, grossOpenAmount),
|
concentration_top_counterparty_pct: percentageOfTotal(topCounterparties[0]?.total_amount ?? 0, grossOpenAmount),
|
||||||
concentration_top_contract_pct: percentageOfTotal(topContracts[0]?.total_amount ?? 0, grossOpenAmount),
|
concentration_top_contract_pct: percentageOfTotal(topContracts[0]?.total_amount ?? 0, grossOpenAmount),
|
||||||
|
age_signal: ageSignal,
|
||||||
inference_basis: "open_contracts_confirmed_1c_balance_rows"
|
inference_basis: "open_contracts_confirmed_1c_balance_rows"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -2335,6 +2444,12 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
||||||
? ` Крупнейший открытый договор: ${leader.contract}${leader.counterparty ? ` / ${leader.counterparty}` : ""} — ${leader.total_amount_human_ru}${leaderShareText}.`
|
? ` Крупнейший открытый договор: ${leader.contract}${leader.counterparty ? ` / ${leader.counterparty}` : ""} — ${leader.total_amount_human_ru}${leaderShareText}.`
|
||||||
: "";
|
: "";
|
||||||
facts.push(`Качество открытых расчетов на ${quality.as_of_date} проверено по договорным остаткам 60/62/76: брутто ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${leaderText}`);
|
facts.push(`Качество открытых расчетов на ${quality.as_of_date} проверено по договорным остаткам 60/62/76: брутто ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${leaderText}`);
|
||||||
|
if (quality.age_signal?.oldest_start_date) {
|
||||||
|
const ageText = quality.age_signal.max_age_days === null
|
||||||
|
? ""
|
||||||
|
: `, максимальный возраст сигнала ${quality.age_signal.max_age_days} дн.`;
|
||||||
|
facts.push(`Возрастной сигнал открытых расчетов подтвержден по датам договоров: самая ранняя дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не договорная просрочка и не due-date анализ.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (derived.inventory_position) {
|
if (derived.inventory_position) {
|
||||||
const leader = derived.inventory_position.top_items[0];
|
const leader = derived.inventory_position.top_items[0];
|
||||||
|
|
@ -3318,6 +3433,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||||
}
|
}
|
||||||
if (derivedBusinessOverview.debt_open_settlement_quality) {
|
if (derivedBusinessOverview.debt_open_settlement_quality) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
|
||||||
|
if (derivedBusinessOverview.debt_open_settlement_quality.age_signal) {
|
||||||
|
pushReason(reasonCodes, "pilot_derived_business_overview_debt_age_signal_from_contract_dates");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (derivedBusinessOverview.inventory_position) {
|
if (derivedBusinessOverview.inventory_position) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
}
|
}
|
||||||
if (overview.debt_open_settlement_quality) {
|
if (overview.debt_open_settlement_quality) {
|
||||||
families.push("качество открытых расчетов");
|
families.push("качество открытых расчетов");
|
||||||
|
if (overview.debt_open_settlement_quality.age_signal) {
|
||||||
|
families.push("возрастной сигнал открытых расчетов");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (overview.inventory_position) {
|
if (overview.inventory_position) {
|
||||||
families.push("складской срез на дату");
|
families.push("складской срез на дату");
|
||||||
|
|
@ -1020,6 +1023,14 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
||||||
lines.push(
|
lines.push(
|
||||||
`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`
|
`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`
|
||||||
);
|
);
|
||||||
|
if (quality.age_signal?.oldest_start_date) {
|
||||||
|
const ageText = quality.age_signal.max_age_days === null
|
||||||
|
? ""
|
||||||
|
: `, максимальный возраст сигнала ${quality.age_signal.max_age_days} дн.`;
|
||||||
|
lines.push(
|
||||||
|
`Возрастной сигнал открытых расчетов: самая ранняя найденная дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не просрочка и не due-date анализ.`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (overview.inventory_position) {
|
if (overview.inventory_position) {
|
||||||
const leader = overview.inventory_position.top_items[0];
|
const leader = overview.inventory_position.top_items[0];
|
||||||
|
|
@ -1111,6 +1122,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
}
|
}
|
||||||
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
|
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
|
||||||
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
|
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
|
||||||
|
if (pilot.derived_business_overview.debt_open_settlement_quality.age_signal) {
|
||||||
|
pushReason(reasonCodes, "answer_contains_business_overview_debt_age_signal");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pilot.derived_business_overview?.inventory_position) {
|
if (pilot.derived_business_overview?.inventory_position) {
|
||||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||||
|
|
|
||||||
|
|
@ -201,12 +201,32 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition {
|
||||||
export interface AssistantMcpDiscoveryBusinessOverviewDebtOpenContractBucket {
|
export interface AssistantMcpDiscoveryBusinessOverviewDebtOpenContractBucket {
|
||||||
contract: string;
|
contract: string;
|
||||||
counterparty: string | null;
|
counterparty: string | null;
|
||||||
|
contract_start_date: string | null;
|
||||||
rows_with_amount: number;
|
rows_with_amount: number;
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
total_amount_human_ru: string;
|
total_amount_human_ru: string;
|
||||||
share_of_gross_open_amount_pct: number | null;
|
share_of_gross_open_amount_pct: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryBusinessOverviewDebtAgeContractBucket {
|
||||||
|
contract: string;
|
||||||
|
counterparty: string | null;
|
||||||
|
start_date: string;
|
||||||
|
age_days: number | null;
|
||||||
|
total_amount: number;
|
||||||
|
total_amount_human_ru: string;
|
||||||
|
share_of_gross_open_amount_pct: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryBusinessOverviewDebtAgeSignal {
|
||||||
|
contracts_with_start_date: number;
|
||||||
|
oldest_start_date: string | null;
|
||||||
|
latest_start_date: string | null;
|
||||||
|
max_age_days: number | null;
|
||||||
|
top_aged_contracts: AssistantMcpDiscoveryBusinessOverviewDebtAgeContractBucket[];
|
||||||
|
inference_basis: "contract_dates_from_open_settlement_rows";
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality {
|
export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality {
|
||||||
as_of_date: string;
|
as_of_date: string;
|
||||||
rows_matched: number;
|
rows_matched: number;
|
||||||
|
|
@ -221,6 +241,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQ
|
||||||
top_contracts: AssistantMcpDiscoveryBusinessOverviewDebtOpenContractBucket[];
|
top_contracts: AssistantMcpDiscoveryBusinessOverviewDebtOpenContractBucket[];
|
||||||
concentration_top_counterparty_pct: number | null;
|
concentration_top_counterparty_pct: number | null;
|
||||||
concentration_top_contract_pct: number | null;
|
concentration_top_contract_pct: number | null;
|
||||||
|
age_signal: AssistantMcpDiscoveryBusinessOverviewDebtAgeSignal | null;
|
||||||
inference_basis: "open_contracts_confirmed_1c_balance_rows";
|
inference_basis: "open_contracts_confirmed_1c_balance_rows";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2160,6 +2181,72 @@ function rowCounterpartyValue(row: Record<string, unknown>): string | null {
|
||||||
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDateParts(yearText: string, monthText: string, dayText: string): string | null {
|
||||||
|
const year = Number(yearText);
|
||||||
|
const month = Number(monthText);
|
||||||
|
const day = Number(dayText);
|
||||||
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > 31) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
date.getUTCFullYear() !== year ||
|
||||||
|
date.getUTCMonth() !== month - 1 ||
|
||||||
|
date.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
String(year).padStart(4, "0"),
|
||||||
|
String(month).padStart(2, "0"),
|
||||||
|
String(day).padStart(2, "0")
|
||||||
|
].join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractContractDateFromText(value: string | null): string | null {
|
||||||
|
const text = toNonEmptyString(value);
|
||||||
|
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
|
||||||
|
if (isoLikeMatch) {
|
||||||
|
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||||
|
}
|
||||||
|
const ruDateMatch = text.match(/(\d{1,2})[./-](\d{1,2})[./-](\d{4})/);
|
||||||
|
if (ruDateMatch) {
|
||||||
|
return normalizeDateParts(ruDateMatch[3], ruDateMatch[2], ruDateMatch[1]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function earlierIsoDate(left: string | null, right: string | null): string | null {
|
||||||
|
if (!left) {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
if (!right) {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
return right < left ? right : left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowOpenSettlementContractStartDateValue(row: Record<string, unknown>): string | null {
|
||||||
|
const candidates = [
|
||||||
|
rowContractValue(row),
|
||||||
|
rowDocumentValue(row),
|
||||||
|
...rowAnalyticsTextValues(row)
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const contractDate = extractContractDateFromText(candidate);
|
||||||
|
if (contractDate) {
|
||||||
|
return contractDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
function monthBucketFromIsoDate(isoDate: string | null): string | null {
|
||||||
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
|
||||||
return match ? `${match[1]}-${match[2]}` : null;
|
return match ? `${match[1]}-${match[2]}` : null;
|
||||||
|
|
@ -2685,10 +2772,11 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debtAsOfDate = input.debtAsOfDate;
|
||||||
const counterpartyBuckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
const counterpartyBuckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||||
const contractBuckets = new Map<
|
const contractBuckets = new Map<
|
||||||
string,
|
string,
|
||||||
{ counterparty: string | null; rows_with_amount: number; total_amount: number }
|
{ counterparty: string | null; contract_start_date: string | null; rows_with_amount: number; total_amount: number }
|
||||||
>();
|
>();
|
||||||
const counterparties = new Set<string>();
|
const counterparties = new Set<string>();
|
||||||
const contracts = new Set<string>();
|
const contracts = new Set<string>();
|
||||||
|
|
@ -2722,15 +2810,18 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contract) {
|
if (contract) {
|
||||||
|
const contractStartDate = rowOpenSettlementContractStartDateValue(row);
|
||||||
contracts.add(contract);
|
contracts.add(contract);
|
||||||
const current = contractBuckets.get(contract) ?? {
|
const current = contractBuckets.get(contract) ?? {
|
||||||
counterparty,
|
counterparty,
|
||||||
|
contract_start_date: contractStartDate,
|
||||||
rows_with_amount: 0,
|
rows_with_amount: 0,
|
||||||
total_amount: 0
|
total_amount: 0
|
||||||
};
|
};
|
||||||
if (!current.counterparty && counterparty) {
|
if (!current.counterparty && counterparty) {
|
||||||
current.counterparty = counterparty;
|
current.counterparty = counterparty;
|
||||||
}
|
}
|
||||||
|
current.contract_start_date = earlierIsoDate(current.contract_start_date, contractStartDate);
|
||||||
current.rows_with_amount += 1;
|
current.rows_with_amount += 1;
|
||||||
current.total_amount += absAmount;
|
current.total_amount += absAmount;
|
||||||
contractBuckets.set(contract, current);
|
contractBuckets.set(contract, current);
|
||||||
|
|
@ -2760,6 +2851,7 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input: {
|
||||||
.map(([contract, bucket]) => ({
|
.map(([contract, bucket]) => ({
|
||||||
contract,
|
contract,
|
||||||
counterparty: bucket.counterparty,
|
counterparty: bucket.counterparty,
|
||||||
|
contract_start_date: bucket.contract_start_date,
|
||||||
rows_with_amount: bucket.rows_with_amount,
|
rows_with_amount: bucket.rows_with_amount,
|
||||||
total_amount: bucket.total_amount,
|
total_amount: bucket.total_amount,
|
||||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||||
|
|
@ -2771,8 +2863,52 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input: {
|
||||||
})
|
})
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
|
|
||||||
|
const agedContractBuckets = Array.from(contractBuckets.entries())
|
||||||
|
.map(([contract, bucket]): AssistantMcpDiscoveryBusinessOverviewDebtAgeContractBucket | null => {
|
||||||
|
if (!bucket.contract_start_date) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contract,
|
||||||
|
counterparty: bucket.counterparty,
|
||||||
|
start_date: bucket.contract_start_date,
|
||||||
|
age_days: daysBetweenIsoDates(bucket.contract_start_date, debtAsOfDate),
|
||||||
|
total_amount: bucket.total_amount,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||||
|
share_of_gross_open_amount_pct: percentageOfTotal(bucket.total_amount, grossOpenAmount)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is AssistantMcpDiscoveryBusinessOverviewDebtAgeContractBucket => Boolean(item))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftAge = left.age_days ?? -1;
|
||||||
|
const rightAge = right.age_days ?? -1;
|
||||||
|
const ageDelta = rightAge - leftAge;
|
||||||
|
if (ageDelta !== 0) {
|
||||||
|
return ageDelta;
|
||||||
|
}
|
||||||
|
const amountDelta = right.total_amount - left.total_amount;
|
||||||
|
return amountDelta !== 0 ? amountDelta : left.contract.localeCompare(right.contract, "ru");
|
||||||
|
});
|
||||||
|
const debtAgeDates = agedContractBuckets.map((bucket) => bucket.start_date).sort();
|
||||||
|
const maxAgeDays = agedContractBuckets.reduce<number | null>((max, bucket) => {
|
||||||
|
if (bucket.age_days === null) {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
return max === null ? bucket.age_days : Math.max(max, bucket.age_days);
|
||||||
|
}, null);
|
||||||
|
const ageSignal: AssistantMcpDiscoveryBusinessOverviewDebtAgeSignal | null = agedContractBuckets.length > 0
|
||||||
|
? {
|
||||||
|
contracts_with_start_date: agedContractBuckets.length,
|
||||||
|
oldest_start_date: debtAgeDates[0] ?? null,
|
||||||
|
latest_start_date: debtAgeDates[debtAgeDates.length - 1] ?? null,
|
||||||
|
max_age_days: maxAgeDays,
|
||||||
|
top_aged_contracts: agedContractBuckets.slice(0, 5),
|
||||||
|
inference_basis: "contract_dates_from_open_settlement_rows"
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
as_of_date: input.debtAsOfDate,
|
as_of_date: debtAsOfDate,
|
||||||
rows_matched: input.openContractsResult.matched_rows,
|
rows_matched: input.openContractsResult.matched_rows,
|
||||||
rows_with_amount: rowsWithAmount,
|
rows_with_amount: rowsWithAmount,
|
||||||
gross_open_amount: grossOpenAmount,
|
gross_open_amount: grossOpenAmount,
|
||||||
|
|
@ -2785,6 +2921,7 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input: {
|
||||||
top_contracts: topContracts,
|
top_contracts: topContracts,
|
||||||
concentration_top_counterparty_pct: percentageOfTotal(topCounterparties[0]?.total_amount ?? 0, grossOpenAmount),
|
concentration_top_counterparty_pct: percentageOfTotal(topCounterparties[0]?.total_amount ?? 0, grossOpenAmount),
|
||||||
concentration_top_contract_pct: percentageOfTotal(topContracts[0]?.total_amount ?? 0, grossOpenAmount),
|
concentration_top_contract_pct: percentageOfTotal(topContracts[0]?.total_amount ?? 0, grossOpenAmount),
|
||||||
|
age_signal: ageSignal,
|
||||||
inference_basis: "open_contracts_confirmed_1c_balance_rows"
|
inference_basis: "open_contracts_confirmed_1c_balance_rows"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -3114,6 +3251,14 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||||
facts.push(
|
facts.push(
|
||||||
`Качество открытых расчетов на ${quality.as_of_date} проверено по договорным остаткам 60/62/76: брутто ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${leaderText}`
|
`Качество открытых расчетов на ${quality.as_of_date} проверено по договорным остаткам 60/62/76: брутто ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${leaderText}`
|
||||||
);
|
);
|
||||||
|
if (quality.age_signal?.oldest_start_date) {
|
||||||
|
const ageText = quality.age_signal.max_age_days === null
|
||||||
|
? ""
|
||||||
|
: `, максимальный возраст сигнала ${quality.age_signal.max_age_days} дн.`;
|
||||||
|
facts.push(
|
||||||
|
`Возрастной сигнал открытых расчетов подтвержден по датам договоров: самая ранняя дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не договорная просрочка и не due-date анализ.`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (derived.inventory_position) {
|
if (derived.inventory_position) {
|
||||||
const leader = derived.inventory_position.top_items[0];
|
const leader = derived.inventory_position.top_items[0];
|
||||||
|
|
@ -4225,6 +4370,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||||
}
|
}
|
||||||
if (derivedBusinessOverview.debt_open_settlement_quality) {
|
if (derivedBusinessOverview.debt_open_settlement_quality) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
|
||||||
|
if (derivedBusinessOverview.debt_open_settlement_quality.age_signal) {
|
||||||
|
pushReason(reasonCodes, "pilot_derived_business_overview_debt_age_signal_from_contract_dates");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (derivedBusinessOverview.inventory_position) {
|
if (derivedBusinessOverview.inventory_position) {
|
||||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||||
|
|
|
||||||
|
|
@ -361,8 +361,8 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А" },
|
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" },
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 50000, Counterparty: "Поставщик А", Contract: "Договор Б" }
|
{ Period: "2020-12-31T00:00:00", Amount: 50000, Counterparty: "Поставщик А", Contract: "Договор Б от 15.03.2020" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
|
@ -380,12 +380,16 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
|
|
||||||
expect(draft.headline).toContain("долговой срез");
|
expect(draft.headline).toContain("долговой срез");
|
||||||
expect(draft.headline).toContain("качество открытых расчетов");
|
expect(draft.headline).toContain("качество открытых расчетов");
|
||||||
|
expect(draft.headline).toContain("возрастной сигнал открытых расчетов");
|
||||||
expect(draft.confirmed_lines.join("\n")).toContain("Долговой срез на 2020-12-31");
|
expect(draft.confirmed_lines.join("\n")).toContain("Долговой срез на 2020-12-31");
|
||||||
expect(draft.confirmed_lines.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
|
expect(draft.confirmed_lines.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
|
||||||
|
expect(draft.confirmed_lines.join("\n")).toContain("Возрастной сигнал открытых расчетов");
|
||||||
|
expect(draft.confirmed_lines.join("\n")).toContain("не due-date анализ");
|
||||||
expect(draft.confirmed_lines.join("\n")).toContain("нетто");
|
expect(draft.confirmed_lines.join("\n")).toContain("нетто");
|
||||||
expect(draft.unknown_lines.join("\n")).toContain("due-date");
|
expect(draft.unknown_lines.join("\n")).toContain("due-date");
|
||||||
expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_position");
|
expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_position");
|
||||||
expect(draft.reason_codes).toContain("answer_contains_business_overview_open_settlement_quality");
|
expect(draft.reason_codes).toContain("answer_contains_business_overview_open_settlement_quality");
|
||||||
|
expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_age_signal");
|
||||||
expect(draft.must_not_claim).toContain("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
expect(draft.must_not_claim).toContain("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||||
expect(draft.must_not_claim).toContain("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
expect(draft.must_not_claim).toContain("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -326,8 +326,8 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rows: [
|
rows: [
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 90000, Counterparty: "Клиент А", Contract: "Договор А" },
|
{ Period: "2020-12-31T00:00:00", Amount: 90000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" },
|
||||||
{ Period: "2020-12-31T00:00:00", Amount: 30000, Counterparty: "Клиент Б", Contract: "Договор Б" }
|
{ Period: "2020-12-31T00:00:00", Amount: 30000, Counterparty: "Клиент Б", Contract: "Договор Б от 15.03.2020" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ rows: [] },
|
{ rows: [] },
|
||||||
|
|
@ -368,16 +368,31 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
unique_contracts: 2,
|
unique_contracts: 2,
|
||||||
concentration_top_contract_pct: 75
|
concentration_top_contract_pct: 75
|
||||||
});
|
});
|
||||||
|
expect(result.derived_business_overview?.debt_open_settlement_quality?.age_signal).toMatchObject({
|
||||||
|
contracts_with_start_date: 2,
|
||||||
|
oldest_start_date: "2019-02-10",
|
||||||
|
latest_start_date: "2020-03-15",
|
||||||
|
max_age_days: 690
|
||||||
|
});
|
||||||
|
expect(result.derived_business_overview?.debt_open_settlement_quality?.age_signal?.top_aged_contracts[0]).toMatchObject({
|
||||||
|
contract: "Договор А от 10.02.2019",
|
||||||
|
start_date: "2019-02-10",
|
||||||
|
age_days: 690,
|
||||||
|
total_amount: 90000
|
||||||
|
});
|
||||||
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_position");
|
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_position");
|
||||||
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_open_settlement_quality");
|
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_open_settlement_quality");
|
||||||
expect(result.derived_business_overview?.missing_signal_families).toContain("debt_due_date_aging_quality");
|
expect(result.derived_business_overview?.missing_signal_families).toContain("debt_due_date_aging_quality");
|
||||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("Долговая позиция на 2020-12-31");
|
expect(result.evidence.confirmed_facts.join("\n")).toContain("Долговая позиция на 2020-12-31");
|
||||||
expect(result.evidence.confirmed_facts.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
|
expect(result.evidence.confirmed_facts.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
|
||||||
|
expect(result.evidence.confirmed_facts.join("\n")).toContain("Возрастной сигнал открытых расчетов");
|
||||||
|
expect(result.evidence.confirmed_facts.join("\n")).toContain("не due-date анализ");
|
||||||
expect(result.evidence.unknown_facts.join("\n")).toContain("due-date");
|
expect(result.evidence.unknown_facts.join("\n")).toContain("due-date");
|
||||||
expect(result.reason_codes).toContain("pilot_business_overview_debt_query_mcp_executed");
|
expect(result.reason_codes).toContain("pilot_business_overview_debt_query_mcp_executed");
|
||||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_position_from_confirmed_rows");
|
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_position_from_confirmed_rows");
|
||||||
expect(result.reason_codes).toContain("pilot_business_overview_open_contracts_query_mcp_executed");
|
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_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(9);
|
||||||
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
|
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
|
||||||
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue