ARCH: восстановить годовое покрытие MCP discovery помесячными пробами
This commit is contained in:
parent
4baa54fe81
commit
cd3315e06d
|
|
@ -1304,6 +1304,47 @@ Module progress:
|
|||
|
||||
- Big Block 5 MCP Semantic Data Agent: `100%`.
|
||||
|
||||
## Progress Update - 2026-04-21 MCP Discovery Yearly Coverage Recovery Through Monthly Probes
|
||||
|
||||
The twentieth implementation slice of Big Block 5 closes the first yearly-coverage proof gap that remained inside guarded MCP discovery value-flow answers.
|
||||
|
||||
Before this slice the assistant could already:
|
||||
|
||||
- answer incoming counterparty turnover totals through guarded MCP discovery;
|
||||
- answer outgoing supplier payout totals through guarded MCP discovery;
|
||||
- compose bidirectional net totals and month-by-month net lines from the same evidence.
|
||||
|
||||
But the broad yearly value-flow probe still stopped at the first MCP row limit. For dense 2020 counterparty activity this meant the assistant answered honestly, yet underpowered: user-facing text still said the outgoing side hit `100 из 100`, even though the missing coverage was recoverable through bounded subperiod probes rather than impossible.
|
||||
|
||||
New behavior:
|
||||
|
||||
- the discovery planner now grants a larger but still bounded probe budget for explicit yearly value-flow and monthly-aggregation questions;
|
||||
- the pilot executor first runs the broad yearly value-flow probe and, only when that broad probe hits the row limit on an explicit year window, automatically recovers coverage through month-by-month 1C subprobes;
|
||||
- recovered subperiod rows are stitched into one coverage-aware result instead of being exposed as separate runtime fragments;
|
||||
- guarded answer drafting keeps the answer honest: it no longer claims a partial year when monthly recovery fully proves the requested period, but it still states the derivation basis and keeps "outside the checked period is not proven" boundaries intact.
|
||||
|
||||
Replay result:
|
||||
|
||||
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun14` passed `8/8`, final status `accepted`;
|
||||
- the supplier payout step now answers the 2020 `Группа СВК` question with `43 763 351,53 руб.`, based on `299 из 299` recovered rows from `2020-01-09` through `2020-12-25`;
|
||||
- the bidirectional net step now answers with `47 628 853,03 руб.` received, `43 763 351,53 руб.` paid, and `3 865 501,50 руб.` net in our favor for the requested 2020 window;
|
||||
- the monthly net step keeps the month-by-month shape from January through December 2020 and no longer inherits the old fake partiality caused by the first broad outgoing probe hitting the row cap;
|
||||
- user-facing text explains that coverage was restored through monthly 1C checks after the broad probe hit the row limit, without leaking `planner_`, `pilot_`, `runtime_`, `query_documents`, or `query_movements`.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed `91/91`;
|
||||
- `npm run build` passed;
|
||||
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun14 --timeout-seconds 240` passed `8/8`, final status `accepted`.
|
||||
|
||||
Known remaining boundary:
|
||||
|
||||
- this still does not make Qwen3 an unrestricted self-navigating 1C agent across arbitrary registers and schemas; richer follow-up drilldown over the recovered rows, broader self-navigation, and evidence-safe agentic exploration remain future work beyond this first completed guarded MCP semantic data block.
|
||||
|
||||
Module progress:
|
||||
|
||||
- Big Block 5 MCP Semantic Data Agent: `100%`.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@
|
|||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)100\\s+из\\s+100",
|
||||
"(?i)лимит.*строк",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
|
|
@ -95,6 +97,8 @@
|
|||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)100\\s+из\\s+100",
|
||||
"(?i)лимит.*строк",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
|
|
@ -125,6 +129,8 @@
|
|||
"forbidden_answer_patterns": [
|
||||
"(?i)точный маршрут.*не подключ",
|
||||
"(?i)не буду подставлять",
|
||||
"(?i)100\\s+из\\s+100",
|
||||
"(?i)лимит.*строк",
|
||||
"(?i)query_documents",
|
||||
"(?i)query_movements",
|
||||
"(?i)runtime_",
|
||||
|
|
|
|||
|
|
@ -168,6 +168,149 @@ function queryResultToProbeResult(primitiveId, result) {
|
|||
limitation: result.error
|
||||
};
|
||||
}
|
||||
function toCoverageAwareQueryResult(result, options = {}) {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
coverage_limited_by_probe_limit: options.coverageLimitedByProbeLimit ?? false,
|
||||
coverage_recovered_by_period_chunking: options.coverageRecoveredByPeriodChunking ?? false,
|
||||
period_chunking_granularity: options.periodChunkingGranularity ?? null,
|
||||
period_chunk_count: options.periodChunkCount ?? 0
|
||||
};
|
||||
}
|
||||
function monthWindowsForYear(year) {
|
||||
const result = [];
|
||||
for (let month = 0; month < 12; month += 1) {
|
||||
const start = new Date(Date.UTC(Number(year), month, 1));
|
||||
const end = new Date(Date.UTC(Number(year), month + 1, 0));
|
||||
result.push({
|
||||
period_from: `${start.getUTCFullYear()}-${String(start.getUTCMonth() + 1).padStart(2, "0")}-${String(start.getUTCDate()).padStart(2, "0")}`,
|
||||
period_to: `${end.getUTCFullYear()}-${String(end.getUTCMonth() + 1).padStart(2, "0")}-${String(end.getUTCDate()).padStart(2, "0")}`
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function periodWindowsForDateScope(dateScope) {
|
||||
const yearMatch = dateScope?.match(/^(\d{4})$/);
|
||||
if (yearMatch) {
|
||||
return monthWindowsForYear(yearMatch[1]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function mergeCoverageAwareQueryResults(results, options) {
|
||||
const rawRows = results.flatMap((item) => item.raw_rows);
|
||||
const rows = results.flatMap((item) => item.rows);
|
||||
const errors = results.map((item) => toNonEmptyString(item.error)).filter((item) => Boolean(item));
|
||||
return {
|
||||
fetched_rows: results.reduce((sum, item) => sum + item.fetched_rows, 0),
|
||||
matched_rows: results.reduce((sum, item) => sum + item.matched_rows, 0),
|
||||
raw_rows: rawRows,
|
||||
rows,
|
||||
error: errors[0] ?? null,
|
||||
coverage_limited_by_probe_limit: options.coverageLimitedByProbeLimit,
|
||||
coverage_recovered_by_period_chunking: options.coverageRecoveredByPeriodChunking,
|
||||
period_chunking_granularity: options.periodChunkingGranularity,
|
||||
period_chunk_count: options.periodChunkCount
|
||||
};
|
||||
}
|
||||
async function executeCoverageAwareValueFlowQuery(input) {
|
||||
const queryLimitations = [];
|
||||
const probeResults = [];
|
||||
let executedProbeCount = 0;
|
||||
const broadRecipePlan = input.recipePlanBuilder(input.baseFilters);
|
||||
const broadResult = await input.deps.executeAddressMcpQuery({
|
||||
query: broadRecipePlan.query,
|
||||
limit: broadRecipePlan.limit,
|
||||
account_scope: broadRecipePlan.account_scope
|
||||
});
|
||||
executedProbeCount += 1;
|
||||
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
|
||||
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= input.maxRowsPerProbe;
|
||||
if (broadResult.error) {
|
||||
pushUnique(queryLimitations, broadResult.error);
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: false
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
const periodWindows = periodWindowsForDateScope(input.dateScope);
|
||||
if (!broadCoverageLimited || periodWindows.length === 0) {
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: broadCoverageLimited
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
const requiredChunkProbeCount = periodWindows.length;
|
||||
if (executedProbeCount + requiredChunkProbeCount > input.maxProbeCount) {
|
||||
pushUnique(queryLimitations, "Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count");
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: true
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
const chunkResults = [];
|
||||
let anyChunkLimited = false;
|
||||
let anyChunkError = false;
|
||||
for (const window of periodWindows) {
|
||||
const chunkFilters = {
|
||||
...input.baseFilters,
|
||||
period_from: window.period_from,
|
||||
period_to: window.period_to
|
||||
};
|
||||
const chunkPlan = input.recipePlanBuilder(chunkFilters);
|
||||
const chunkResult = await input.deps.executeAddressMcpQuery({
|
||||
query: chunkPlan.query,
|
||||
limit: chunkPlan.limit,
|
||||
account_scope: chunkPlan.account_scope
|
||||
});
|
||||
executedProbeCount += 1;
|
||||
probeResults.push(queryResultToProbeResult(input.primitiveId, chunkResult));
|
||||
if (chunkResult.error) {
|
||||
anyChunkError = true;
|
||||
pushUnique(queryLimitations, chunkResult.error);
|
||||
continue;
|
||||
}
|
||||
if (chunkResult.matched_rows >= input.maxRowsPerProbe) {
|
||||
anyChunkLimited = true;
|
||||
}
|
||||
chunkResults.push(chunkResult);
|
||||
}
|
||||
if (chunkResults.length === 0 && anyChunkError) {
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: true
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
return {
|
||||
result: mergeCoverageAwareQueryResults(chunkResults, {
|
||||
coverageLimitedByProbeLimit: anyChunkLimited || anyChunkError,
|
||||
coverageRecoveredByPeriodChunking: true,
|
||||
periodChunkingGranularity: "month",
|
||||
periodChunkCount: periodWindows.length
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
function summarizeLifecycleRows(result) {
|
||||
if (result.error) {
|
||||
return null;
|
||||
|
|
@ -184,6 +327,9 @@ function summarizeValueFlowRows(result) {
|
|||
if (result.fetched_rows <= 0) {
|
||||
return "0 MCP value-flow rows fetched";
|
||||
}
|
||||
if (result.coverage_recovered_by_period_chunking && result.period_chunking_granularity === "month") {
|
||||
return `${result.period_chunk_count} monthly MCP value-flow probes fetched ${result.fetched_rows} rows total, ${result.matched_rows} matched value-flow scope after the broad probe hit the row limit`;
|
||||
}
|
||||
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
|
||||
}
|
||||
function rowDateValue(row) {
|
||||
|
|
@ -370,7 +516,7 @@ function deriveBidirectionalValueFlowMonthBreakdown(input) {
|
|||
};
|
||||
});
|
||||
}
|
||||
function deriveValueFlow(result, counterparty, periodScope, direction, probeLimit, aggregationAxis) {
|
||||
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -401,12 +547,14 @@ function deriveValueFlow(result, counterparty, periodScope, direction, probeLimi
|
|||
total_amount_human_ru: formatAmountHumanRu(totalAmount),
|
||||
first_movement_date: dates[0] ?? null,
|
||||
latest_movement_date: dates[dates.length - 1] ?? null,
|
||||
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit,
|
||||
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity: result.period_chunking_granularity,
|
||||
monthly_breakdown: deriveValueFlowMonthBreakdown(result, aggregationAxis),
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
function deriveValueFlowSideSummary(result, probeLimit) {
|
||||
function deriveValueFlowSideSummary(result) {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return {
|
||||
rows_matched: 0,
|
||||
|
|
@ -415,7 +563,9 @@ function deriveValueFlowSideSummary(result, probeLimit) {
|
|||
total_amount_human_ru: formatAmountHumanRu(0),
|
||||
first_movement_date: null,
|
||||
latest_movement_date: null,
|
||||
coverage_limited_by_probe_limit: false
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: false,
|
||||
period_chunking_granularity: null
|
||||
};
|
||||
}
|
||||
let totalAmount = 0;
|
||||
|
|
@ -438,12 +588,14 @@ function deriveValueFlowSideSummary(result, probeLimit) {
|
|||
total_amount_human_ru: formatAmountHumanRu(totalAmount),
|
||||
first_movement_date: dates[0] ?? null,
|
||||
latest_movement_date: dates[dates.length - 1] ?? null,
|
||||
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit
|
||||
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity: result.period_chunking_granularity
|
||||
};
|
||||
}
|
||||
function deriveBidirectionalValueFlow(input) {
|
||||
const incoming = deriveValueFlowSideSummary(input.incomingResult, input.probeLimit);
|
||||
const outgoing = deriveValueFlowSideSummary(input.outgoingResult, input.probeLimit);
|
||||
const incoming = deriveValueFlowSideSummary(input.incomingResult);
|
||||
const outgoing = deriveValueFlowSideSummary(input.outgoingResult);
|
||||
if (incoming.rows_with_amount <= 0 && outgoing.rows_with_amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -458,6 +610,8 @@ function deriveBidirectionalValueFlow(input) {
|
|||
net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)),
|
||||
net_direction: netDirectionFromAmount(netAmount),
|
||||
coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking: incoming.coverage_recovered_by_period_chunking || outgoing.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity: incoming.period_chunking_granularity ?? outgoing.period_chunking_granularity ?? null,
|
||||
monthly_breakdown: deriveBidirectionalValueFlowMonthBreakdown({
|
||||
incomingResult: input.incomingResult,
|
||||
outgoingResult: input.outgoingResult,
|
||||
|
|
@ -474,10 +628,14 @@ function summarizeBidirectionalValueFlowRows(input) {
|
|||
}
|
||||
const incomingSummary = incoming?.error
|
||||
? "incoming value-flow query failed"
|
||||
: `${incoming?.fetched_rows ?? 0} incoming value-flow rows fetched, ${incoming?.matched_rows ?? 0} matched`;
|
||||
: incoming?.coverage_recovered_by_period_chunking && incoming.period_chunking_granularity === "month"
|
||||
? `${incoming.period_chunk_count} monthly incoming value-flow probes fetched ${incoming.fetched_rows} rows total, ${incoming.matched_rows} matched`
|
||||
: `${incoming?.fetched_rows ?? 0} incoming value-flow rows fetched, ${incoming?.matched_rows ?? 0} matched`;
|
||||
const outgoingSummary = outgoing?.error
|
||||
? "outgoing supplier-payout query failed"
|
||||
: `${outgoing?.fetched_rows ?? 0} outgoing supplier-payout rows fetched, ${outgoing?.matched_rows ?? 0} matched`;
|
||||
: outgoing?.coverage_recovered_by_period_chunking && outgoing.period_chunking_granularity === "month"
|
||||
? `${outgoing.period_chunk_count} monthly outgoing supplier-payout probes fetched ${outgoing.fetched_rows} rows total, ${outgoing.matched_rows} matched`
|
||||
: `${outgoing?.fetched_rows ?? 0} outgoing supplier-payout rows fetched, ${outgoing?.matched_rows ?? 0} matched`;
|
||||
return `${incomingSummary}; ${outgoingSummary}`;
|
||||
}
|
||||
function buildLifecycleConfirmedFacts(result, counterparty) {
|
||||
|
|
@ -539,6 +697,9 @@ function buildValueFlowInferredFacts(derived) {
|
|||
else {
|
||||
facts.push("Counterparty value-flow total was calculated from confirmed 1C movement rows");
|
||||
}
|
||||
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||
facts.push("Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit");
|
||||
}
|
||||
if (derived.aggregation_axis === "month" && derived.monthly_breakdown.length > 0) {
|
||||
facts.push("Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows");
|
||||
}
|
||||
|
|
@ -549,6 +710,9 @@ function buildBidirectionalValueFlowInferredFacts(derived) {
|
|||
return [];
|
||||
}
|
||||
const facts = ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"];
|
||||
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||
facts.push("Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit");
|
||||
}
|
||||
if (derived.aggregation_axis === "month" && derived.monthly_breakdown.length > 0) {
|
||||
facts.push("Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows");
|
||||
}
|
||||
|
|
@ -706,36 +870,50 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
};
|
||||
}
|
||||
pushReason(reasonCodes, "pilot_bidirectional_value_flow_recipes_selected");
|
||||
const incomingRecipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(incomingSelection.selected_recipe, filters);
|
||||
const outgoingRecipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(outgoingSelection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_movements") {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_bidirectional_value_flow_uses_two_query_movements_and_derives_net"));
|
||||
continue;
|
||||
}
|
||||
incomingResult = await deps.executeAddressMcpQuery({
|
||||
query: incomingRecipePlan.query,
|
||||
limit: incomingRecipePlan.limit,
|
||||
account_scope: incomingRecipePlan.account_scope
|
||||
const incomingExecution = await executeCoverageAwareValueFlowQuery({
|
||||
primitiveId: step.primitive_id,
|
||||
recipePlanBuilder: (scopedFilters) => (0, addressRecipeCatalog_1.buildAddressRecipePlan)(incomingSelection.selected_recipe, scopedFilters),
|
||||
baseFilters: filters,
|
||||
dateScope,
|
||||
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
|
||||
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
deps
|
||||
});
|
||||
outgoingResult = await deps.executeAddressMcpQuery({
|
||||
query: outgoingRecipePlan.query,
|
||||
limit: outgoingRecipePlan.limit,
|
||||
account_scope: outgoingRecipePlan.account_scope
|
||||
const outgoingExecution = await executeCoverageAwareValueFlowQuery({
|
||||
primitiveId: step.primitive_id,
|
||||
recipePlanBuilder: (scopedFilters) => (0, addressRecipeCatalog_1.buildAddressRecipePlan)(outgoingSelection.selected_recipe, scopedFilters),
|
||||
baseFilters: filters,
|
||||
dateScope,
|
||||
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
|
||||
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
deps
|
||||
});
|
||||
incomingResult = incomingExecution.result;
|
||||
outgoingResult = outgoingExecution.result;
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, incomingResult));
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, outgoingResult));
|
||||
if (incomingResult.error) {
|
||||
pushUnique(queryLimitations, incomingResult.error);
|
||||
probeResults.push(...incomingExecution.probe_results, ...outgoingExecution.probe_results);
|
||||
for (const limitation of [...incomingExecution.query_limitations, ...outgoingExecution.query_limitations]) {
|
||||
pushUnique(queryLimitations, limitation);
|
||||
}
|
||||
if (incomingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_incoming_query_movements_mcp_error");
|
||||
}
|
||||
if (outgoingResult.error) {
|
||||
pushUnique(queryLimitations, outgoingResult.error);
|
||||
if (outgoingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_outgoing_query_movements_mcp_error");
|
||||
}
|
||||
if (!incomingResult.error || !outgoingResult.error) {
|
||||
if (incomingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_incoming_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (outgoingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (!incomingResult?.error || !outgoingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_query_movements_mcp_executed");
|
||||
}
|
||||
}
|
||||
|
|
@ -745,7 +923,6 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
outgoingResult,
|
||||
counterparty,
|
||||
periodScope: dateScope,
|
||||
probeLimit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
aggregationAxis
|
||||
});
|
||||
if (derivedBidirectionalValueFlow) {
|
||||
|
|
@ -810,30 +987,39 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
|||
pushReason(reasonCodes, valueFlowProfile.direction === "outgoing_supplier_payout"
|
||||
? "pilot_supplier_payout_recipe_selected"
|
||||
: "pilot_customer_revenue_recipe_selected");
|
||||
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_movements") {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_value_flow_uses_query_movements_and_derives_aggregate"));
|
||||
continue;
|
||||
}
|
||||
queryResult = await deps.executeAddressMcpQuery({
|
||||
query: recipePlan.query,
|
||||
limit: recipePlan.limit,
|
||||
account_scope: recipePlan.account_scope
|
||||
const execution = await executeCoverageAwareValueFlowQuery({
|
||||
primitiveId: step.primitive_id,
|
||||
recipePlanBuilder: (scopedFilters) => (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, scopedFilters),
|
||||
baseFilters: filters,
|
||||
dateScope,
|
||||
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
|
||||
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
deps
|
||||
});
|
||||
executedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
|
||||
if (queryResult.error) {
|
||||
pushUnique(queryLimitations, queryResult.error);
|
||||
queryResult = execution.result;
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(...execution.probe_results);
|
||||
for (const limitation of execution.query_limitations) {
|
||||
pushUnique(queryLimitations, limitation);
|
||||
}
|
||||
if (queryResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
|
||||
}
|
||||
if (queryResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
}
|
||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, planner.discovery_plan.execution_budget.max_rows_per_probe, aggregationAxis);
|
||||
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, aggregationAxis);
|
||||
if (derivedValueFlow) {
|
||||
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
|
||||
if (aggregationAxis === "month" && derivedValueFlow.monthly_breakdown.length > 0) {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,24 @@ function addScopeAxes(axes, meaning) {
|
|||
function includesAny(text, tokens) {
|
||||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
function isYearDateScope(meaning) {
|
||||
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
|
||||
}
|
||||
function budgetOverrideFor(input, recipe) {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
const isValueFlowRecipe = recipe.semanticDataNeed === "counterparty value-flow evidence" &&
|
||||
recipe.primitives.includes("query_movements");
|
||||
if (!isValueFlowRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: 30
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
function recipeFor(input) {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
|
|
@ -136,14 +154,19 @@ function statusFrom(plan, review) {
|
|||
}
|
||||
function planAssistantMcpDiscovery(input) {
|
||||
const recipe = recipeFor(input);
|
||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const reasonCodes = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
if (budgetOverride.maxProbeCount) {
|
||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||
}
|
||||
const plan = (0, assistantMcpDiscoveryPolicy_1.buildAssistantMcpDiscoveryPlan)({
|
||||
semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes
|
||||
requiredAxes: recipe.axes,
|
||||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const DEFAULT_DISCOVERY_BUDGET = {
|
|||
max_probe_count: 3,
|
||||
max_rows_per_probe: 100
|
||||
};
|
||||
const MAX_PROBE_COUNT = 6;
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
const ALLOWED_PRIMITIVE_SET = new Set(exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
|
||||
function toNonEmptyString(value) {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,12 @@ function localizeLine(value) {
|
|||
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
}
|
||||
if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function section(title, lines) {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
|
|||
first_movement_date: string | null;
|
||||
latest_movement_date: string | null;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
coverage_recovered_by_period_chunking: boolean;
|
||||
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
monthly_breakdown: AssistantMcpDiscoveryValueFlowMonthBucket[];
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
|
@ -74,6 +76,8 @@ export interface AssistantMcpDiscoveryValueFlowSideSummary {
|
|||
first_movement_date: string | null;
|
||||
latest_movement_date: string | null;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
coverage_recovered_by_period_chunking: boolean;
|
||||
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket {
|
||||
|
|
@ -99,10 +103,26 @@ export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
|
|||
net_amount_human_ru: string;
|
||||
net_direction: AssistantMcpDiscoveryNetDirection;
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
coverage_recovered_by_period_chunking: boolean;
|
||||
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
monthly_breakdown: AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket[];
|
||||
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows";
|
||||
}
|
||||
|
||||
interface AssistantMcpDiscoveryCoverageAwareQueryResult extends AddressMcpQueryExecutorResult {
|
||||
coverage_limited_by_probe_limit: boolean;
|
||||
coverage_recovered_by_period_chunking: boolean;
|
||||
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
period_chunk_count: number;
|
||||
}
|
||||
|
||||
interface AssistantMcpDiscoveryCoverageAwareQueryExecution {
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
probe_results: AssistantMcpDiscoveryProbeResult[];
|
||||
query_limitations: string[];
|
||||
executed_probe_count: number;
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryPilotScope =
|
||||
| "counterparty_lifecycle_query_documents_v1"
|
||||
| "counterparty_value_flow_query_movements_v1"
|
||||
|
|
@ -330,6 +350,193 @@ function queryResultToProbeResult(
|
|||
};
|
||||
}
|
||||
|
||||
function toCoverageAwareQueryResult(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
options: {
|
||||
coverageLimitedByProbeLimit?: boolean;
|
||||
coverageRecoveredByPeriodChunking?: boolean;
|
||||
periodChunkingGranularity?: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
periodChunkCount?: number;
|
||||
} = {}
|
||||
): AssistantMcpDiscoveryCoverageAwareQueryResult | null {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
coverage_limited_by_probe_limit: options.coverageLimitedByProbeLimit ?? false,
|
||||
coverage_recovered_by_period_chunking: options.coverageRecoveredByPeriodChunking ?? false,
|
||||
period_chunking_granularity: options.periodChunkingGranularity ?? null,
|
||||
period_chunk_count: options.periodChunkCount ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
function monthWindowsForYear(year: string): Array<{ period_from: string; period_to: string }> {
|
||||
const result: Array<{ period_from: string; period_to: string }> = [];
|
||||
for (let month = 0; month < 12; month += 1) {
|
||||
const start = new Date(Date.UTC(Number(year), month, 1));
|
||||
const end = new Date(Date.UTC(Number(year), month + 1, 0));
|
||||
result.push({
|
||||
period_from: `${start.getUTCFullYear()}-${String(start.getUTCMonth() + 1).padStart(2, "0")}-${String(start.getUTCDate()).padStart(2, "0")}`,
|
||||
period_to: `${end.getUTCFullYear()}-${String(end.getUTCMonth() + 1).padStart(2, "0")}-${String(end.getUTCDate()).padStart(2, "0")}`
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function periodWindowsForDateScope(dateScope: string | null): Array<{ period_from: string; period_to: string }> {
|
||||
const yearMatch = dateScope?.match(/^(\d{4})$/);
|
||||
if (yearMatch) {
|
||||
return monthWindowsForYear(yearMatch[1]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function mergeCoverageAwareQueryResults(
|
||||
results: AddressMcpQueryExecutorResult[],
|
||||
options: {
|
||||
coverageLimitedByProbeLimit: boolean;
|
||||
coverageRecoveredByPeriodChunking: boolean;
|
||||
periodChunkingGranularity: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
periodChunkCount: number;
|
||||
}
|
||||
): AssistantMcpDiscoveryCoverageAwareQueryResult {
|
||||
const rawRows = results.flatMap((item) => item.raw_rows);
|
||||
const rows = results.flatMap((item) => item.rows);
|
||||
const errors = results.map((item) => toNonEmptyString(item.error)).filter((item): item is string => Boolean(item));
|
||||
return {
|
||||
fetched_rows: results.reduce((sum, item) => sum + item.fetched_rows, 0),
|
||||
matched_rows: results.reduce((sum, item) => sum + item.matched_rows, 0),
|
||||
raw_rows: rawRows,
|
||||
rows,
|
||||
error: errors[0] ?? null,
|
||||
coverage_limited_by_probe_limit: options.coverageLimitedByProbeLimit,
|
||||
coverage_recovered_by_period_chunking: options.coverageRecoveredByPeriodChunking,
|
||||
period_chunking_granularity: options.periodChunkingGranularity,
|
||||
period_chunk_count: options.periodChunkCount
|
||||
};
|
||||
}
|
||||
|
||||
async function executeCoverageAwareValueFlowQuery(input: {
|
||||
primitiveId: string;
|
||||
recipePlanBuilder: (filters: AddressFilterSet) => {
|
||||
query: string;
|
||||
limit: number;
|
||||
account_scope?: string[];
|
||||
};
|
||||
baseFilters: AddressFilterSet;
|
||||
dateScope: string | null;
|
||||
maxProbeCount: number;
|
||||
maxRowsPerProbe: number;
|
||||
deps: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||
}): Promise<AssistantMcpDiscoveryCoverageAwareQueryExecution> {
|
||||
const queryLimitations: string[] = [];
|
||||
const probeResults: AssistantMcpDiscoveryProbeResult[] = [];
|
||||
let executedProbeCount = 0;
|
||||
|
||||
const broadRecipePlan = input.recipePlanBuilder(input.baseFilters);
|
||||
const broadResult = await input.deps.executeAddressMcpQuery({
|
||||
query: broadRecipePlan.query,
|
||||
limit: broadRecipePlan.limit,
|
||||
account_scope: broadRecipePlan.account_scope
|
||||
});
|
||||
executedProbeCount += 1;
|
||||
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
|
||||
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= input.maxRowsPerProbe;
|
||||
|
||||
if (broadResult.error) {
|
||||
pushUnique(queryLimitations, broadResult.error);
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: false
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
|
||||
const periodWindows = periodWindowsForDateScope(input.dateScope);
|
||||
if (!broadCoverageLimited || periodWindows.length === 0) {
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: broadCoverageLimited
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
|
||||
const requiredChunkProbeCount = periodWindows.length;
|
||||
if (executedProbeCount + requiredChunkProbeCount > input.maxProbeCount) {
|
||||
pushUnique(
|
||||
queryLimitations,
|
||||
"Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count"
|
||||
);
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: true
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
|
||||
const chunkResults: AddressMcpQueryExecutorResult[] = [];
|
||||
let anyChunkLimited = false;
|
||||
let anyChunkError = false;
|
||||
|
||||
for (const window of periodWindows) {
|
||||
const chunkFilters: AddressFilterSet = {
|
||||
...input.baseFilters,
|
||||
period_from: window.period_from,
|
||||
period_to: window.period_to
|
||||
};
|
||||
const chunkPlan = input.recipePlanBuilder(chunkFilters);
|
||||
const chunkResult = await input.deps.executeAddressMcpQuery({
|
||||
query: chunkPlan.query,
|
||||
limit: chunkPlan.limit,
|
||||
account_scope: chunkPlan.account_scope
|
||||
});
|
||||
executedProbeCount += 1;
|
||||
probeResults.push(queryResultToProbeResult(input.primitiveId, chunkResult));
|
||||
if (chunkResult.error) {
|
||||
anyChunkError = true;
|
||||
pushUnique(queryLimitations, chunkResult.error);
|
||||
continue;
|
||||
}
|
||||
if (chunkResult.matched_rows >= input.maxRowsPerProbe) {
|
||||
anyChunkLimited = true;
|
||||
}
|
||||
chunkResults.push(chunkResult);
|
||||
}
|
||||
|
||||
if (chunkResults.length === 0 && anyChunkError) {
|
||||
return {
|
||||
result: toCoverageAwareQueryResult(broadResult, {
|
||||
coverageLimitedByProbeLimit: true
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
result: mergeCoverageAwareQueryResults(chunkResults, {
|
||||
coverageLimitedByProbeLimit: anyChunkLimited || anyChunkError,
|
||||
coverageRecoveredByPeriodChunking: true,
|
||||
periodChunkingGranularity: "month",
|
||||
periodChunkCount: periodWindows.length
|
||||
}),
|
||||
probe_results: probeResults,
|
||||
query_limitations: queryLimitations,
|
||||
executed_probe_count: executedProbeCount
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeLifecycleRows(result: AddressMcpQueryExecutorResult): string | null {
|
||||
if (result.error) {
|
||||
return null;
|
||||
|
|
@ -340,13 +547,16 @@ function summarizeLifecycleRows(result: AddressMcpQueryExecutorResult): string |
|
|||
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
|
||||
}
|
||||
|
||||
function summarizeValueFlowRows(result: AddressMcpQueryExecutorResult): string | null {
|
||||
function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryResult): string | null {
|
||||
if (result.error) {
|
||||
return null;
|
||||
}
|
||||
if (result.fetched_rows <= 0) {
|
||||
return "0 MCP value-flow rows fetched";
|
||||
}
|
||||
if (result.coverage_recovered_by_period_chunking && result.period_chunking_granularity === "month") {
|
||||
return `${result.period_chunk_count} monthly MCP value-flow probes fetched ${result.fetched_rows} rows total, ${result.matched_rows} matched value-flow scope after the broad probe hit the row limit`;
|
||||
}
|
||||
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
|
||||
}
|
||||
|
||||
|
|
@ -482,7 +692,7 @@ function formatAmountHumanRu(amount: number): string {
|
|||
}
|
||||
|
||||
function deriveValueFlowMonthBreakdown(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||
): AssistantMcpDiscoveryValueFlowMonthBucket[] {
|
||||
if (!result || result.error || aggregationAxis !== "month") {
|
||||
|
|
@ -512,8 +722,8 @@ function deriveValueFlowMonthBreakdown(
|
|||
}
|
||||
|
||||
function deriveBidirectionalValueFlowMonthBreakdown(input: {
|
||||
incomingResult: AddressMcpQueryExecutorResult | null;
|
||||
outgoingResult: AddressMcpQueryExecutorResult | null;
|
||||
incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
}): AssistantMcpDiscoveryBidirectionalValueFlowMonthBucket[] {
|
||||
if (input.aggregationAxis !== "month") {
|
||||
|
|
@ -557,11 +767,10 @@ function deriveBidirectionalValueFlowMonthBreakdown(input: {
|
|||
}
|
||||
|
||||
function deriveValueFlow(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
|
||||
counterparty: string | null,
|
||||
periodScope: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||
probeLimit: number,
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
|
|
@ -594,15 +803,16 @@ function deriveValueFlow(
|
|||
total_amount_human_ru: formatAmountHumanRu(totalAmount),
|
||||
first_movement_date: dates[0] ?? null,
|
||||
latest_movement_date: dates[dates.length - 1] ?? null,
|
||||
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit,
|
||||
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity: result.period_chunking_granularity,
|
||||
monthly_breakdown: deriveValueFlowMonthBreakdown(result, aggregationAxis),
|
||||
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function deriveValueFlowSideSummary(
|
||||
result: AddressMcpQueryExecutorResult | null,
|
||||
probeLimit: number
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
|
||||
): AssistantMcpDiscoveryValueFlowSideSummary {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
return {
|
||||
|
|
@ -612,7 +822,9 @@ function deriveValueFlowSideSummary(
|
|||
total_amount_human_ru: formatAmountHumanRu(0),
|
||||
first_movement_date: null,
|
||||
latest_movement_date: null,
|
||||
coverage_limited_by_probe_limit: false
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: false,
|
||||
period_chunking_granularity: null
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -636,20 +848,21 @@ function deriveValueFlowSideSummary(
|
|||
total_amount_human_ru: formatAmountHumanRu(totalAmount),
|
||||
first_movement_date: dates[0] ?? null,
|
||||
latest_movement_date: dates[dates.length - 1] ?? null,
|
||||
coverage_limited_by_probe_limit: result.matched_rows >= probeLimit
|
||||
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity: result.period_chunking_granularity
|
||||
};
|
||||
}
|
||||
|
||||
function deriveBidirectionalValueFlow(input: {
|
||||
incomingResult: AddressMcpQueryExecutorResult | null;
|
||||
outgoingResult: AddressMcpQueryExecutorResult | null;
|
||||
incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
counterparty: string | null;
|
||||
periodScope: string | null;
|
||||
probeLimit: number;
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null;
|
||||
}): AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null {
|
||||
const incoming = deriveValueFlowSideSummary(input.incomingResult, input.probeLimit);
|
||||
const outgoing = deriveValueFlowSideSummary(input.outgoingResult, input.probeLimit);
|
||||
const incoming = deriveValueFlowSideSummary(input.incomingResult);
|
||||
const outgoing = deriveValueFlowSideSummary(input.outgoingResult);
|
||||
if (incoming.rows_with_amount <= 0 && outgoing.rows_with_amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -665,6 +878,10 @@ function deriveBidirectionalValueFlow(input: {
|
|||
net_direction: netDirectionFromAmount(netAmount),
|
||||
coverage_limited_by_probe_limit:
|
||||
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
|
||||
coverage_recovered_by_period_chunking:
|
||||
incoming.coverage_recovered_by_period_chunking || outgoing.coverage_recovered_by_period_chunking,
|
||||
period_chunking_granularity:
|
||||
incoming.period_chunking_granularity ?? outgoing.period_chunking_granularity ?? null,
|
||||
monthly_breakdown: deriveBidirectionalValueFlowMonthBreakdown({
|
||||
incomingResult: input.incomingResult,
|
||||
outgoingResult: input.outgoingResult,
|
||||
|
|
@ -675,8 +892,8 @@ function deriveBidirectionalValueFlow(input: {
|
|||
}
|
||||
|
||||
function summarizeBidirectionalValueFlowRows(input: {
|
||||
incomingResult: AddressMcpQueryExecutorResult | null;
|
||||
outgoingResult: AddressMcpQueryExecutorResult | null;
|
||||
incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
|
||||
}): string | null {
|
||||
const incoming = input.incomingResult;
|
||||
const outgoing = input.outgoingResult;
|
||||
|
|
@ -685,10 +902,14 @@ function summarizeBidirectionalValueFlowRows(input: {
|
|||
}
|
||||
const incomingSummary = incoming?.error
|
||||
? "incoming value-flow query failed"
|
||||
: `${incoming?.fetched_rows ?? 0} incoming value-flow rows fetched, ${incoming?.matched_rows ?? 0} matched`;
|
||||
: incoming?.coverage_recovered_by_period_chunking && incoming.period_chunking_granularity === "month"
|
||||
? `${incoming.period_chunk_count} monthly incoming value-flow probes fetched ${incoming.fetched_rows} rows total, ${incoming.matched_rows} matched`
|
||||
: `${incoming?.fetched_rows ?? 0} incoming value-flow rows fetched, ${incoming?.matched_rows ?? 0} matched`;
|
||||
const outgoingSummary = outgoing?.error
|
||||
? "outgoing supplier-payout query failed"
|
||||
: `${outgoing?.fetched_rows ?? 0} outgoing supplier-payout rows fetched, ${outgoing?.matched_rows ?? 0} matched`;
|
||||
: outgoing?.coverage_recovered_by_period_chunking && outgoing.period_chunking_granularity === "month"
|
||||
? `${outgoing.period_chunk_count} monthly outgoing supplier-payout probes fetched ${outgoing.fetched_rows} rows total, ${outgoing.matched_rows} matched`
|
||||
: `${outgoing?.fetched_rows ?? 0} outgoing supplier-payout rows fetched, ${outgoing?.matched_rows ?? 0} matched`;
|
||||
return `${incomingSummary}; ${outgoingSummary}`;
|
||||
}
|
||||
|
||||
|
|
@ -704,7 +925,7 @@ function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, cou
|
|||
}
|
||||
|
||||
function buildValueFlowConfirmedFacts(
|
||||
result: AddressMcpQueryExecutorResult,
|
||||
result: AssistantMcpDiscoveryCoverageAwareQueryResult,
|
||||
counterparty: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
||||
): string[] {
|
||||
|
|
@ -760,6 +981,11 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
|||
} else {
|
||||
facts.push("Counterparty value-flow total was calculated from confirmed 1C movement rows");
|
||||
}
|
||||
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||
facts.push(
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
);
|
||||
}
|
||||
if (derived.aggregation_axis === "month" && derived.monthly_breakdown.length > 0) {
|
||||
facts.push("Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows");
|
||||
}
|
||||
|
|
@ -773,6 +999,11 @@ function buildBidirectionalValueFlowInferredFacts(
|
|||
return [];
|
||||
}
|
||||
const facts = ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"];
|
||||
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
|
||||
facts.push(
|
||||
"Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit"
|
||||
);
|
||||
}
|
||||
if (derived.aggregation_axis === "month" && derived.monthly_breakdown.length > 0) {
|
||||
facts.push("Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows");
|
||||
}
|
||||
|
|
@ -933,12 +1164,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
const aggregationAxis = aggregationAxisForPlanner(planner);
|
||||
|
||||
if (valueFlowPilotEligible) {
|
||||
let queryResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let queryResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
||||
const filters = buildValueFlowFilters(planner);
|
||||
const valueFlowProfile = valueFlowPilotProfile(planner);
|
||||
if (valueFlowProfile.direction === "bidirectional_net_value_flow") {
|
||||
let incomingResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let outgoingResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
||||
let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
|
||||
const incomingSelection = selectAddressRecipe("customer_revenue_and_payments", filters);
|
||||
const outgoingSelection = selectAddressRecipe("supplier_payouts_profile", filters);
|
||||
if (!incomingSelection.selected_recipe || !outgoingSelection.selected_recipe) {
|
||||
|
|
@ -965,8 +1196,6 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
}
|
||||
|
||||
pushReason(reasonCodes, "pilot_bidirectional_value_flow_recipes_selected");
|
||||
const incomingRecipePlan = buildAddressRecipePlan(incomingSelection.selected_recipe, filters);
|
||||
const outgoingRecipePlan = buildAddressRecipePlan(outgoingSelection.selected_recipe, filters);
|
||||
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_movements") {
|
||||
|
|
@ -977,28 +1206,44 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
continue;
|
||||
}
|
||||
|
||||
incomingResult = await deps.executeAddressMcpQuery({
|
||||
query: incomingRecipePlan.query,
|
||||
limit: incomingRecipePlan.limit,
|
||||
account_scope: incomingRecipePlan.account_scope
|
||||
const incomingExecution = await executeCoverageAwareValueFlowQuery({
|
||||
primitiveId: step.primitive_id,
|
||||
recipePlanBuilder: (scopedFilters) => buildAddressRecipePlan(incomingSelection.selected_recipe!, scopedFilters),
|
||||
baseFilters: filters,
|
||||
dateScope,
|
||||
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
|
||||
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
deps
|
||||
});
|
||||
outgoingResult = await deps.executeAddressMcpQuery({
|
||||
query: outgoingRecipePlan.query,
|
||||
limit: outgoingRecipePlan.limit,
|
||||
account_scope: outgoingRecipePlan.account_scope
|
||||
const outgoingExecution = await executeCoverageAwareValueFlowQuery({
|
||||
primitiveId: step.primitive_id,
|
||||
recipePlanBuilder: (scopedFilters) => buildAddressRecipePlan(outgoingSelection.selected_recipe!, scopedFilters),
|
||||
baseFilters: filters,
|
||||
dateScope,
|
||||
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
|
||||
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
deps
|
||||
});
|
||||
incomingResult = incomingExecution.result;
|
||||
outgoingResult = outgoingExecution.result;
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, incomingResult));
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, outgoingResult));
|
||||
if (incomingResult.error) {
|
||||
pushUnique(queryLimitations, incomingResult.error);
|
||||
probeResults.push(...incomingExecution.probe_results, ...outgoingExecution.probe_results);
|
||||
for (const limitation of [...incomingExecution.query_limitations, ...outgoingExecution.query_limitations]) {
|
||||
pushUnique(queryLimitations, limitation);
|
||||
}
|
||||
if (incomingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_incoming_query_movements_mcp_error");
|
||||
}
|
||||
if (outgoingResult.error) {
|
||||
pushUnique(queryLimitations, outgoingResult.error);
|
||||
if (outgoingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_outgoing_query_movements_mcp_error");
|
||||
}
|
||||
if (!incomingResult.error || !outgoingResult.error) {
|
||||
if (incomingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_incoming_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (outgoingResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
if (!incomingResult?.error || !outgoingResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_bidirectional_query_movements_mcp_executed");
|
||||
}
|
||||
}
|
||||
|
|
@ -1009,7 +1254,6 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
outgoingResult,
|
||||
counterparty,
|
||||
periodScope: dateScope,
|
||||
probeLimit: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
aggregationAxis
|
||||
});
|
||||
if (derivedBidirectionalValueFlow) {
|
||||
|
|
@ -1080,26 +1324,35 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
: "pilot_customer_revenue_recipe_selected"
|
||||
);
|
||||
|
||||
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id !== "query_movements") {
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(skippedProbeResult(step, "pilot_value_flow_uses_query_movements_and_derives_aggregate"));
|
||||
continue;
|
||||
}
|
||||
queryResult = await deps.executeAddressMcpQuery({
|
||||
query: recipePlan.query,
|
||||
limit: recipePlan.limit,
|
||||
account_scope: recipePlan.account_scope
|
||||
const execution = await executeCoverageAwareValueFlowQuery({
|
||||
primitiveId: step.primitive_id,
|
||||
recipePlanBuilder: (scopedFilters) => buildAddressRecipePlan(selection.selected_recipe!, scopedFilters),
|
||||
baseFilters: filters,
|
||||
dateScope,
|
||||
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
|
||||
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
deps
|
||||
});
|
||||
executedPrimitives.push(step.primitive_id);
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
|
||||
if (queryResult.error) {
|
||||
pushUnique(queryLimitations, queryResult.error);
|
||||
queryResult = execution.result;
|
||||
pushUnique(executedPrimitives, step.primitive_id);
|
||||
probeResults.push(...execution.probe_results);
|
||||
for (const limitation of execution.query_limitations) {
|
||||
pushUnique(queryLimitations, limitation);
|
||||
}
|
||||
if (queryResult?.error) {
|
||||
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
|
||||
}
|
||||
if (queryResult?.coverage_recovered_by_period_chunking) {
|
||||
pushReason(reasonCodes, "pilot_monthly_period_chunking_recovered_coverage");
|
||||
}
|
||||
}
|
||||
|
||||
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
|
||||
|
|
@ -1108,7 +1361,6 @@ export async function executeAssistantMcpDiscoveryPilot(
|
|||
counterparty,
|
||||
dateScope,
|
||||
valueFlowProfile.direction,
|
||||
planner.discovery_plan.execution_budget.max_rows_per_probe,
|
||||
aggregationAxis
|
||||
);
|
||||
if (derivedValueFlow) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ interface PlannerRecipe {
|
|||
reason: string;
|
||||
}
|
||||
|
||||
interface PlannerBudgetOverride {
|
||||
maxProbeCount?: number;
|
||||
}
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
|
|
@ -96,6 +100,27 @@ function includesAny(text: string, tokens: string[]): boolean {
|
|||
return tokens.some((token) => text.includes(token));
|
||||
}
|
||||
|
||||
function isYearDateScope(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
|
||||
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
|
||||
}
|
||||
|
||||
function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: PlannerRecipe): PlannerBudgetOverride {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const requestedAggregationAxis = aggregationAxis(meaning);
|
||||
const isValueFlowRecipe =
|
||||
recipe.semanticDataNeed === "counterparty value-flow evidence" &&
|
||||
recipe.primitives.includes("query_movements");
|
||||
if (!isValueFlowRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: 30
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
||||
const meaning = input.turnMeaning ?? null;
|
||||
const domain = lower(meaning?.asked_domain_family);
|
||||
|
|
@ -190,15 +215,20 @@ export function planAssistantMcpDiscovery(
|
|||
input: AssistantMcpDiscoveryPlannerInput
|
||||
): AssistantMcpDiscoveryPlannerContract {
|
||||
const recipe = recipeFor(input);
|
||||
const budgetOverride = budgetOverrideFor(input, recipe);
|
||||
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
|
||||
const reasonCodes: string[] = [];
|
||||
pushReason(reasonCodes, recipe.reason);
|
||||
if (budgetOverride.maxProbeCount) {
|
||||
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
|
||||
}
|
||||
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed,
|
||||
turnMeaning: input.turnMeaning,
|
||||
proposedPrimitives: recipe.primitives,
|
||||
requiredAxes: recipe.axes
|
||||
requiredAxes: recipe.axes,
|
||||
maxProbeCount: budgetOverride.maxProbeCount
|
||||
});
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
const plannerStatus = statusFrom(plan, review);
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
|||
max_rows_per_probe: 100
|
||||
};
|
||||
|
||||
const MAX_PROBE_COUNT = 6;
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
|
||||
const ALLOWED_PRIMITIVE_SET = new Set<string>(ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
|
||||
|
|
|
|||
|
|
@ -173,6 +173,20 @@ function localizeLine(value: string): string {
|
|||
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (
|
||||
/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
}
|
||||
if (
|
||||
/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,45 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.reason_codes).toContain("answer_contains_monthly_breakdown");
|
||||
});
|
||||
|
||||
it("keeps recovered yearly coverage out of the unknown block and explains the recovery as bounded inference", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const broadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
const monthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
|
||||
Amount: (index + 1) * 100,
|
||||
Counterparty: "SVK"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([{ rows: broadRows }, ...monthlyResults])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.inference_lines).toContain(
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
);
|
||||
expect(draft.unknown_lines).not.toContain(
|
||||
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not leak primitive names or query text into user-facing lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -199,6 +199,56 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("recovers yearly value-flow coverage by splitting a limited broad probe into monthly subprobes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||
}
|
||||
});
|
||||
const broadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
const monthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
|
||||
Amount: (index + 1) * 100,
|
||||
Counterparty: "SVK"
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([{ rows: broadRows }, ...monthlyResults])
|
||||
);
|
||||
|
||||
expect(result.derived_value_flow).toMatchObject({
|
||||
value_flow_direction: "outgoing_supplier_payout",
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month",
|
||||
rows_matched: 12,
|
||||
rows_with_amount: 12,
|
||||
total_amount: 7800,
|
||||
first_movement_date: "2020-01-05",
|
||||
latest_movement_date: "2020-12-05"
|
||||
});
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
);
|
||||
expect(result.evidence.unknown_facts).not.toContain(
|
||||
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_monthly_period_chunking_recovered_coverage");
|
||||
});
|
||||
|
||||
it("executes bidirectional value-flow queries and derives guarded net cash flow", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
@ -319,6 +369,69 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(result.reason_codes).toContain("pilot_derived_bidirectional_monthly_breakdown_from_confirmed_rows");
|
||||
});
|
||||
|
||||
it("recovers bidirectional yearly coverage when one side is rebuilt from monthly subprobes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const outgoingBroadRows = Array.from({ length: 100 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 10,
|
||||
Counterparty: "SVK"
|
||||
}));
|
||||
const outgoingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-10T00:00:00`,
|
||||
Amount: (index + 1) * 50,
|
||||
Counterparty: "SVK"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: 10000, Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{ rows: outgoingBroadRows },
|
||||
...outgoingMonthlyResults
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_bidirectional_value_flow).toMatchObject({
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month",
|
||||
net_amount: 16100,
|
||||
incoming_customer_revenue: {
|
||||
total_amount: 20000,
|
||||
coverage_limited_by_probe_limit: false
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount: 3900,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month"
|
||||
}
|
||||
});
|
||||
expect(result.evidence.inferred_facts).toContain(
|
||||
"Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit"
|
||||
);
|
||||
expect(result.evidence.unknown_facts).not.toContain(
|
||||
"Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_bidirectional_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
|
||||
});
|
||||
|
||||
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
|
||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
|
||||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
||||
});
|
||||
|
||||
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
|
||||
|
|
@ -67,6 +69,7 @@ describe("assistant MCP discovery planner", () => {
|
|||
"calendar_month"
|
||||
]);
|
||||
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_recipe");
|
||||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
});
|
||||
|
||||
it("builds a document discovery plan without falling back to movement primitives", () => {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe("assistant MCP discovery policy", () => {
|
|||
expect(plan.rejected_primitives).toEqual(["drop_database"]);
|
||||
expect(plan.requires_evidence_gate).toBe(true);
|
||||
expect(plan.answer_may_use_raw_model_claims).toBe(false);
|
||||
expect(plan.execution_budget).toEqual({ max_probe_count: 6, max_rows_per_probe: 500 });
|
||||
expect(plan.execution_budget).toEqual({ max_probe_count: 36, max_rows_per_probe: 500 });
|
||||
expect(plan.reason_codes).toContain("model_proposed_unregistered_mcp_primitive");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -182,6 +182,33 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.reply_text).not.toContain("Counterparty monthly net value-flow breakdown");
|
||||
});
|
||||
|
||||
it("localizes recovered coverage facts without leaking broad-probe wording", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
|
||||
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
|
||||
inference_lines: [
|
||||
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
|
||||
],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("Покрытие запрошенного периода восстановлено помесячными проверками 1С");
|
||||
expect(candidate.reply_text).not.toContain("broad probe hit the row limit");
|
||||
});
|
||||
|
||||
it("returns not applicable when discovery was skipped for an exact supported route", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue