ARCH: ввести data-need graph и довести open-scope comparison до live replay

This commit is contained in:
2026-04-22 20:38:36 +03:00
parent dca49ef4e1
commit f2bd2dfdb1
27 changed files with 2832 additions and 43 deletions
@@ -141,6 +141,16 @@ function isMovementLaneClarification(pilot) {
askedActionFamily(pilot) === "list_movements" ||
unsupportedFamily(pilot) === "movement_evidence");
}
function isRankedValueFlowClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph"));
}
function isBidirectionalValueFlowComparisonClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"));
}
function isDocumentLaneClarification(pilot) {
return (isDocumentPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
@@ -152,12 +162,20 @@ function laneScopeSuffix(pilot) {
const entity = firstEntityCandidate(pilot);
return entity ? ` по "${entity}"` : "";
}
function dryRunHasAxis(pilot, axis) {
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
}
function dryRunMissingAxis(pilot, axis) {
if (dryRunHasAxis(pilot, axis)) {
return false;
}
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
}
function clarificationNeedRu(pilot) {
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
@@ -210,6 +228,9 @@ function headlineFor(mode, pilot) {
pilot.derived_entity_resolution?.resolution_status === "not_found") {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
@@ -269,6 +290,14 @@ function headlineFor(mode, pilot) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@@ -302,6 +331,12 @@ function nextStepFor(mode, pilot) {
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@@ -336,6 +371,10 @@ function buildMustNotClaim(pilot) {
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
}
if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
}
if (isDocumentPilot(pilot)) {
claims.push("Do not claim full document history outside the checked period.");
claims.push("Do not present the confirmed document rows as a complete document universe.");
@@ -463,6 +502,40 @@ function derivedEntityResolutionInferenceLine(pilot) {
}
return null;
}
function derivedRankedValueFlowInferenceLine(pilot) {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking) {
return null;
}
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
}
function derivedRankedValueFlowConfirmedLine(pilot) {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking || ranking.ranked_values.length <= 0) {
return null;
}
const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
const directionLead = ranking.ranking_need === "bottom_asc"
? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту"
: "Меньше всего денег принёс контрагент"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "Больше всего заплатили контрагенту"
: "Больше всего денег принёс контрагент";
const tail = ranking.ranked_values
.slice(1, 3)
.map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`)
.join("; ");
const trail = tail ? ` Следом: ${tail}.` : "";
const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; ranking может быть неполным."
: "";
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
}
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
@@ -553,13 +626,16 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ??
derivedRankedValueFlowConfirmedLine(pilot) ??
derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
: derivedValueFlowMonthlyLines(pilot);
@@ -0,0 +1,286 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function lower(value) {
return String(value ?? "").trim().toLowerCase();
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target, value) {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function businessFactFamilyFor(input) {
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
if (combined.includes("metadata lane clarification")) {
return "schema_surface";
}
if (combined.includes("metadata")) {
return "schema_surface";
}
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
return "entity_grounding";
}
if (combined.includes("lifecycle") || combined.includes("activity")) {
return "activity_lifecycle";
}
if (combined.includes("movement")) {
return "movement_evidence";
}
if (combined.includes("document")) {
return "document_evidence";
}
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
return "value_flow";
}
return null;
}
function aggregationNeedFor(axis) {
if (!axis) {
return null;
}
if (axis === "month") {
return "by_month";
}
return `by_${axis}`;
}
function timeScopeNeedFor(input) {
if (input.explicitDateScope) {
return "explicit_period";
}
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
return "period_required";
}
if (input.family === "activity_lifecycle") {
return "open_activity_window";
}
return null;
}
function comparisonNeedFor(action) {
if (action === "net_value_flow") {
return "incoming_vs_outgoing";
}
return null;
}
function allowsOpenScopeWithoutSubject(input) {
if (input.family !== "value_flow") {
return false;
}
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
}
function rankingNeedFromRawUtterance(value) {
const text = lower(value);
if (!text) {
return null;
}
if (/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(text)) {
return "top_desc";
}
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
return "bottom_asc";
}
return null;
}
function proofExpectationFor(input) {
if (input.clarificationGaps.length > 0) {
return "clarification_required";
}
if (input.family === "schema_surface") {
return "schema_surface";
}
if (input.family === "entity_grounding") {
return "entity_grounding";
}
if (input.family === "activity_lifecycle") {
return "bounded_inference";
}
return "coverage_checked_fact";
}
function decompositionCandidatesFor(input) {
const result = [];
if (input.family === "schema_surface") {
pushUnique(result, "inspect_metadata_surface");
return result;
}
if (input.family === "entity_grounding") {
pushUnique(result, "search_business_entity");
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "value_flow") {
if (input.rankingNeed && input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, "aggregate_ranked_axis_values");
pushUnique(result, "probe_coverage");
return result;
}
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
if (input.aggregationNeed === "by_month") {
pushUnique(result, "aggregate_by_month");
}
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
}
else {
pushUnique(result, "collect_scoped_movements");
}
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "movement_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_movements");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "document_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_documents");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "activity_lifecycle") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_supporting_documents");
pushUnique(result, "probe_coverage");
pushUnique(result, "explain_evidence_basis");
}
return result;
}
function forbiddenOverclaimFlagsFor(family) {
const result = ["no_raw_model_claims"];
if (family === "schema_surface") {
pushUnique(result, "no_fake_schema_surface");
}
if (family === "entity_grounding") {
pushUnique(result, "no_unresolved_entity_claim");
}
if (family === "activity_lifecycle") {
pushUnique(result, "no_legal_age_claim_without_evidence");
}
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
pushUnique(result, "no_unchecked_fact_totals");
}
return result;
}
function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const semanticDataNeed = lower(input.semanticDataNeed);
const turnMeaning = input.turnMeaning ?? null;
const domain = lower(turnMeaning?.asked_domain_family);
const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
const businessFactFamily = businessFactFamilyFor({
semanticDataNeed,
domain,
action,
unsupported
});
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
comparisonNeed,
rankingNeed
});
const clarificationGaps = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
}
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
family: businessFactFamily,
explicitDateScope
});
if (timeScopeNeed === "period_required" && !explicitDateScope) {
pushUnique(clarificationGaps, "period");
}
const decompositionCandidates = decompositionCandidatesFor({
family: businessFactFamily,
action,
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject
});
const reasonCodes = [];
pushReason(reasonCodes, "data_need_graph_built");
if (businessFactFamily) {
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
}
else {
pushReason(reasonCodes, "data_need_graph_family_unknown");
}
if (aggregationNeed) {
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
}
if (rankingNeed) {
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
}
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
aggregation_need: aggregationNeed,
time_scope_need: timeScopeNeed,
comparison_need: comparisonNeed,
ranking_need: rankingNeed,
proof_expectation: proofExpectationFor({
family: businessFactFamily,
clarificationGaps
}),
clarification_gaps: clarificationGaps,
decomposition_candidates: decompositionCandidates,
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
reason_codes: reasonCodes
};
}
@@ -133,6 +133,16 @@ function buildValueFlowFilters(planner) {
sort: "period_asc"
};
}
function organizationScopeForPlanner(planner) {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
function rankingNeedForPlanner(planner) {
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
return rankingNeed;
}
return null;
}
function normalizeEntityResolutionText(value) {
return String(value ?? "")
.toLowerCase()
@@ -313,7 +323,9 @@ function isMovementEvidencePilotEligible(planner) {
combined.includes("list_movements")));
}
function isValueFlowPilotEligible(planner) {
if (planner.selected_chain_id === "value_flow") {
if (planner.selected_chain_id === "value_flow" ||
planner.selected_chain_id === "value_flow_ranking" ||
planner.selected_chain_id === "value_flow_comparison") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
@@ -1040,6 +1052,16 @@ function rowAmountValue(row) {
}
return null;
}
function rowCounterpartyValue(row) {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function monthBucketFromIsoDate(isoDate) {
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
return match ? `${match[1]}-${match[2]}` : null;
@@ -1213,6 +1235,62 @@ function deriveValueFlow(result, counterparty, periodScope, direction, aggregati
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function deriveRankedValueFlow(result, input) {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
const buckets = new Map();
let rowsWithAmount = 0;
for (const row of result.rows) {
const axisValue = rowCounterpartyValue(row);
const amount = rowAmountValue(row);
if (!axisValue || amount === null) {
continue;
}
rowsWithAmount += 1;
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
current.rows_with_amount += 1;
current.total_amount += amount;
buckets.set(axisValue, current);
}
if (rowsWithAmount <= 0 || buckets.size <= 0) {
return null;
}
const rankedValues = Array.from(buckets.entries())
.map(([axisValue, bucket]) => ({
axis_value: axisValue,
rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
}))
.sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount;
if (input.rankingNeed === "bottom_asc") {
if (amountDelta !== 0) {
return -amountDelta;
}
}
else if (amountDelta !== 0) {
return amountDelta;
}
return left.axis_value.localeCompare(right.axis_value, "ru");
})
.slice(0, 5);
return {
value_flow_direction: input.direction,
ranking_need: input.rankingNeed,
ranking_axis: "counterparty",
organization_scope: input.organizationScope,
period_scope: input.periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
ranked_values: rankedValues,
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,
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
};
}
function deriveValueFlowSideSummary(result) {
if (!result || result.error || result.matched_rows <= 0) {
return {
@@ -1345,6 +1423,16 @@ function buildValueFlowConfirmedFacts(result, counterparty, direction) {
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildRankedValueFlowConfirmedFacts(derived) {
if (!derived || derived.ranked_values.length <= 0) {
return [];
}
const leader = derived.ranked_values[0];
const directionLabel = derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
return [
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
];
}
function buildBidirectionalValueFlowConfirmedFacts(derived) {
if (!derived) {
return [];
@@ -1411,6 +1499,16 @@ function buildValueFlowInferredFacts(derived) {
}
return facts;
}
function buildRankedValueFlowInferredFacts(derived) {
if (!derived) {
return [];
}
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
facts.push("Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit");
}
return facts;
}
function buildBidirectionalValueFlowInferredFacts(derived) {
if (!derived) {
return [];
@@ -1453,6 +1551,16 @@ function buildValueFlowUnknownFacts(periodScope, direction, derived) {
: "Full all-time turnover is not proven without an explicit checked period");
return unknownFacts;
}
function buildRankedValueFlowUnknownFacts(periodScope, derived) {
const unknownFacts = [];
if (derived?.coverage_limited_by_probe_limit) {
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
}
unknownFacts.push(periodScope
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time counterparty ranking is not proven without an explicit checked period");
return unknownFacts;
}
function buildBidirectionalValueFlowUnknownFacts(periodScope, derived) {
const unknownFacts = [];
if (derived?.coverage_limited_by_probe_limit) {
@@ -1479,6 +1587,8 @@ function pilotScopeForPlanner(planner) {
return "metadata_inspection_v1";
case "movement_evidence":
return "counterparty_movement_evidence_query_movements_v1";
case "value_flow_comparison":
case "value_flow_ranking":
case "value_flow":
return valueFlowPilotProfile(planner).scope;
case "document_evidence":
@@ -1595,7 +1705,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
}
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const organizationScope = organizationScopeForPlanner(planner);
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (metadataPilotEligible) {
let metadataResult = null;
const metadataScope = metadataScopeForPlanner(planner);
@@ -2151,6 +2263,48 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
organizationScope,
periodScope: dateScope,
direction: valueFlowProfile.direction,
rankingNeed
});
if (derivedRankedValueFlow) {
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: valueFlowProfile.scope,
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: derivedRankedValueFlow,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, aggregationAxis);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
@@ -2183,6 +2337,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
@@ -38,6 +38,9 @@ function pushUnique(target, value) {
function hasEntity(meaning) {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function hasSubjectCandidates(graph) {
return (graph?.subject_candidates.length ?? 0) > 0;
}
function aggregationAxis(meaning) {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
@@ -75,13 +78,137 @@ function budgetOverrideFor(input, recipe) {
}
function recipeFor(input) {
const meaning = input.turnMeaning ?? null;
const dataNeedGraph = input.dataNeedGraph ?? null;
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
chainId: "metadata_lane_clarification",
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
};
}
if (graphFactFamily === "value_flow") {
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "bidirectional value-flow comparison evidence",
chainId: "value_flow_comparison",
chainSummary: "Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
primitives: ["query_movements", "probe_coverage"],
axes,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
};
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "ranked value-flow evidence",
chainId: "value_flow_ranking",
chainSummary: "Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph"
};
}
if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis");
return {
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_from_data_need_graph"
};
}
if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_from_data_need_graph"
};
}
if (graphFactFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_from_data_need_graph"
};
}
if (graphFactFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_from_data_need_graph"
};
}
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe"
};
}
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
return {
@@ -191,8 +318,12 @@ function planAssistantMcpDiscovery(input) {
const recipe = recipeFor(input);
const budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const dataNeedGraph = input.dataNeedGraph ?? null;
const reasonCodes = [];
pushReason(reasonCodes, recipe.reason);
if (dataNeedGraph) {
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
}
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
@@ -219,6 +350,7 @@ function planAssistantMcpDiscovery(input) {
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
data_need_graph: dataNeedGraph,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
@@ -61,6 +61,28 @@ function userFacingLines(values) {
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
}
function localizeLine(value) {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности в запрошенном срезе.";
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
}
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
if (openScopeBidirectionalMatch) {
const incoming = openScopeBidirectionalMatch[1] === "found"
? "входящие строки найдены"
: "входящие строки не найдены";
const outgoing = openScopeBidirectionalMatch[2] === "found"
? "исходящие строки найдены"
: "исходящие строки не найдены";
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
}
if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
}
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) {
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
@@ -51,6 +51,7 @@ function businessFactAnswerAllowed(draft) {
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
semanticDataNeed: input.semanticDataNeed,
dataNeedGraph: input.dataNeedGraph,
turnMeaning: input.turnMeaning
});
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
@@ -62,6 +62,7 @@ async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) {
}
const bridge = await (0, assistantMcpDiscoveryRuntimeBridge_1.runAssistantMcpDiscoveryRuntimeBridge)({
semanticDataNeed: turnInput.semantic_data_need,
dataNeedGraph: turnInput.data_need_graph,
turnMeaning: turnInput.turn_meaning_ref,
deps: input.deps
});
@@ -2,6 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -71,6 +72,9 @@ function compactLower(value) {
.replace(/\s+/g, " ")
.trim();
}
function sameScopedName(left, right) {
return Boolean(left && right && compactLower(left) === compactLower(right));
}
function candidateValue(value) {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
@@ -298,6 +302,9 @@ function hasPayoutSignal(text) {
function hasBidirectionalValueFlowSignal(text) {
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
}
function hasValueRankingSignal(text) {
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(text);
}
function hasMonthlyAggregationSignal(text) {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
}
@@ -551,7 +558,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawText = compactLower(rawSignalSourceText);
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawValueFlowSignal = !rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
@@ -574,6 +582,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty));
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
? null
: predecomposeEntities.counterparty;
const predecomposeDateScope = collectDateScope(predecomposeContract);
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
!rawLifecycleSignal &&
@@ -791,7 +806,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
}
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
}
else {
@@ -801,7 +816,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
}
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
if (!groundedFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
@@ -812,13 +827,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
}
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
? null
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
: null;
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
if (entityCandidates[index] === explicitOrganizationScope) {
entityCandidates.splice(index, 1);
}
}
}
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
const turnMeaning = {
asked_domain_family: lifecycleSignal
@@ -1054,7 +1079,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (followupSeed.counterparty) {
@@ -1072,12 +1098,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
const dataNeedGraph = runDiscovery && hasTurnMeaning
? (0, assistantMcpDiscoveryDataNeedGraph_1.buildAssistantMcpDiscoveryDataNeedGraph)({
semanticDataNeed,
rawUtterance: rawSignalSourceText,
turnMeaning: cleanTurnMeaning
})
: null;
if (dataNeedGraph) {
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
data_need_graph: dataNeedGraph,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes
@@ -192,6 +192,24 @@ function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionC
);
}
function isRankedValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph")
);
}
function isBidirectionalValueFlowComparisonClarification(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): boolean {
return (
pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph")
);
}
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
isDocumentPilot(pilot) ||
@@ -207,7 +225,14 @@ function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): st
return entity ? ` по "${entity}"` : "";
}
function dryRunHasAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
}
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
if (dryRunHasAxis(pilot, axis)) {
return false;
}
return pilot.dry_run.execution_steps.some((step) =>
step.missing_axis_options.some((option) => option.includes(axis))
);
@@ -216,8 +241,10 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
function clarificationNeedRu(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): { subject: string; verb: string } {
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
@@ -281,6 +308,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
) {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С можно построить ограниченный ranking по контрагентам на подтвержденных строках денежных движений.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
@@ -340,6 +370,14 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу сравнить входящий и исходящий денежный поток, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@@ -378,6 +416,12 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@@ -413,6 +457,10 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
}
if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
}
if (isDocumentPilot(pilot)) {
claims.push("Do not claim full document history outside the checked period.");
claims.push("Do not present the confirmed document rows as a complete document universe.");
@@ -556,6 +604,43 @@ function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotE
return null;
}
function derivedRankedValueFlowInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking) {
return null;
}
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
return `Ranking по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
}
function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking || ranking.ranked_values.length <= 0) {
return null;
}
const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
const directionLead =
ranking.ranking_need === "bottom_asc"
? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту"
: "Меньше всего денег принёс контрагент"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "Больше всего заплатили контрагенту"
: "Больше всего денег принёс контрагент";
const tail = ranking.ranked_values
.slice(1, 3)
.map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`)
.join("; ");
const trail = tail ? ` Следом: ${tail}.` : "";
const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; ranking может быть неполным."
: "";
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
}
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_value_flow;
if (!flow) {
@@ -662,13 +747,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
const derivedInferenceLine =
derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const derivedValueLine =
derivedBidirectionalValueFlowConfirmedLine(pilot) ??
derivedRankedValueFlowConfirmedLine(pilot) ??
derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines =
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
@@ -0,0 +1,358 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
"assistant_data_need_graph_v1" as const;
export type AssistantMcpDiscoveryDataNeedProofExpectation =
| "schema_surface"
| "entity_grounding"
| "coverage_checked_fact"
| "bounded_inference"
| "clarification_required";
export interface AssistantMcpDiscoveryDataNeedGraphContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryDataNeedGraph";
subject_candidates: string[];
business_fact_family: string | null;
action_family: string | null;
aggregation_need: string | null;
time_scope_need: string | null;
comparison_need: string | null;
ranking_need: string | null;
proof_expectation: AssistantMcpDiscoveryDataNeedProofExpectation;
clarification_gaps: string[];
decomposition_candidates: string[];
forbidden_overclaim_flags: string[];
reason_codes: string[];
}
export interface BuildAssistantMcpDiscoveryDataNeedGraphInput {
semanticDataNeed?: string | null;
rawUtterance?: string | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function lower(value: unknown): string {
return String(value ?? "").trim().toLowerCase();
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target: string[], value: string | null | undefined): void {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function businessFactFamilyFor(input: {
semanticDataNeed: string;
domain: string;
action: string;
unsupported: string;
}): string | null {
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
if (combined.includes("metadata lane clarification")) {
return "schema_surface";
}
if (combined.includes("metadata")) {
return "schema_surface";
}
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
return "entity_grounding";
}
if (combined.includes("lifecycle") || combined.includes("activity")) {
return "activity_lifecycle";
}
if (combined.includes("movement")) {
return "movement_evidence";
}
if (combined.includes("document")) {
return "document_evidence";
}
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
return "value_flow";
}
return null;
}
function aggregationNeedFor(axis: string): string | null {
if (!axis) {
return null;
}
if (axis === "month") {
return "by_month";
}
return `by_${axis}`;
}
function timeScopeNeedFor(input: {
family: string | null;
explicitDateScope: string | null;
}): string | null {
if (input.explicitDateScope) {
return "explicit_period";
}
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
return "period_required";
}
if (input.family === "activity_lifecycle") {
return "open_activity_window";
}
return null;
}
function comparisonNeedFor(action: string): string | null {
if (action === "net_value_flow") {
return "incoming_vs_outgoing";
}
return null;
}
function allowsOpenScopeWithoutSubject(input: {
family: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
}): boolean {
if (input.family !== "value_flow") {
return false;
}
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
}
function rankingNeedFromRawUtterance(value: string): string | null {
const text = lower(value);
if (!text) {
return null;
}
if (
/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(
text
)
) {
return "top_desc";
}
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
return "bottom_asc";
}
return null;
}
function proofExpectationFor(input: {
family: string | null;
clarificationGaps: string[];
}): AssistantMcpDiscoveryDataNeedProofExpectation {
if (input.clarificationGaps.length > 0) {
return "clarification_required";
}
if (input.family === "schema_surface") {
return "schema_surface";
}
if (input.family === "entity_grounding") {
return "entity_grounding";
}
if (input.family === "activity_lifecycle") {
return "bounded_inference";
}
return "coverage_checked_fact";
}
function decompositionCandidatesFor(input: {
family: string | null;
action: string;
aggregationNeed: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
openScopeWithoutSubject: boolean;
}): string[] {
const result: string[] = [];
if (input.family === "schema_surface") {
pushUnique(result, "inspect_metadata_surface");
return result;
}
if (input.family === "entity_grounding") {
pushUnique(result, "search_business_entity");
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "value_flow") {
if (input.rankingNeed && input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, "aggregate_ranked_axis_values");
pushUnique(result, "probe_coverage");
return result;
}
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
if (input.aggregationNeed === "by_month") {
pushUnique(result, "aggregate_by_month");
}
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
} else {
pushUnique(result, "collect_scoped_movements");
}
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "movement_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_movements");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "document_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_documents");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "activity_lifecycle") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_supporting_documents");
pushUnique(result, "probe_coverage");
pushUnique(result, "explain_evidence_basis");
}
return result;
}
function forbiddenOverclaimFlagsFor(family: string | null): string[] {
const result: string[] = ["no_raw_model_claims"];
if (family === "schema_surface") {
pushUnique(result, "no_fake_schema_surface");
}
if (family === "entity_grounding") {
pushUnique(result, "no_unresolved_entity_claim");
}
if (family === "activity_lifecycle") {
pushUnique(result, "no_legal_age_claim_without_evidence");
}
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
pushUnique(result, "no_unchecked_fact_totals");
}
return result;
}
export function buildAssistantMcpDiscoveryDataNeedGraph(
input: BuildAssistantMcpDiscoveryDataNeedGraphInput
): AssistantMcpDiscoveryDataNeedGraphContract {
const semanticDataNeed = lower(input.semanticDataNeed);
const turnMeaning = input.turnMeaning ?? null;
const domain = lower(turnMeaning?.asked_domain_family);
const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
const businessFactFamily = businessFactFamilyFor({
semanticDataNeed,
domain,
action,
unsupported
});
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
const openScopeWithoutSubject =
subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
comparisonNeed,
rankingNeed
});
const clarificationGaps: string[] = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
}
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
family: businessFactFamily,
explicitDateScope
});
if (timeScopeNeed === "period_required" && !explicitDateScope) {
pushUnique(clarificationGaps, "period");
}
const decompositionCandidates = decompositionCandidatesFor({
family: businessFactFamily,
action,
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject
});
const reasonCodes: string[] = [];
pushReason(reasonCodes, "data_need_graph_built");
if (businessFactFamily) {
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
} else {
pushReason(reasonCodes, "data_need_graph_family_unknown");
}
if (aggregationNeed) {
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
}
if (rankingNeed) {
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
}
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
aggregation_need: aggregationNeed,
time_scope_need: timeScopeNeed,
comparison_need: comparisonNeed,
ranking_need: rankingNeed,
proof_expectation: proofExpectationFor({
family: businessFactFamily,
clarificationGaps
}),
clarification_gaps: clarificationGaps,
decomposition_candidates: decompositionCandidates,
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
reason_codes: reasonCodes
};
}
@@ -78,6 +78,28 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
}
export interface AssistantMcpDiscoveryRankedValueFlowBucket {
axis_value: string;
rows_with_amount: number;
total_amount: number;
total_amount_human_ru: string;
}
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
ranking_need: "top_desc" | "bottom_asc";
ranking_axis: "counterparty";
organization_scope: string | null;
period_scope: string | null;
rows_matched: number;
rows_with_amount: number;
ranked_values: AssistantMcpDiscoveryRankedValueFlowBucket[];
coverage_limited_by_probe_limit: boolean;
coverage_recovered_by_period_chunking: boolean;
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows";
}
export interface AssistantMcpDiscoveryValueFlowSideSummary {
rows_matched: number;
rows_with_amount: number;
@@ -187,6 +209,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null;
derived_entity_resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_ranked_value_flow?: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
query_limitations: string[];
@@ -334,6 +357,20 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A
};
}
function organizationScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
function rankingNeedForPlanner(
planner: AssistantMcpDiscoveryPlannerContract
): AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"] | null {
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
return rankingNeed;
}
return null;
}
function normalizeEntityResolutionText(value: string | null): string {
return String(value ?? "")
.toLowerCase()
@@ -544,7 +581,11 @@ function isMovementEvidencePilotEligible(planner: AssistantMcpDiscoveryPlannerCo
}
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
if (planner.selected_chain_id === "value_flow") {
if (
planner.selected_chain_id === "value_flow" ||
planner.selected_chain_id === "value_flow_ranking" ||
planner.selected_chain_id === "value_flow_comparison"
) {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
@@ -1429,6 +1470,17 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
return null;
}
function rowCounterpartyValue(row: Record<string, unknown>): string | null {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function monthBucketFromIsoDate(isoDate: string | null): string | null {
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
return match ? `${match[1]}-${match[2]}` : null;
@@ -1629,6 +1681,74 @@ function deriveValueFlow(
};
}
function deriveRankedValueFlow(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
input: {
organizationScope: string | null;
periodScope: string | null;
direction: AssistantMcpDiscoveryDerivedRankedValueFlow["value_flow_direction"];
rankingNeed: AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"];
}
): AssistantMcpDiscoveryDerivedRankedValueFlow | null {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
let rowsWithAmount = 0;
for (const row of result.rows) {
const axisValue = rowCounterpartyValue(row);
const amount = rowAmountValue(row);
if (!axisValue || amount === null) {
continue;
}
rowsWithAmount += 1;
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
current.rows_with_amount += 1;
current.total_amount += amount;
buckets.set(axisValue, current);
}
if (rowsWithAmount <= 0 || buckets.size <= 0) {
return null;
}
const rankedValues = Array.from(buckets.entries())
.map(([axisValue, bucket]) => ({
axis_value: axisValue,
rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
}))
.sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount;
if (input.rankingNeed === "bottom_asc") {
if (amountDelta !== 0) {
return -amountDelta;
}
} else if (amountDelta !== 0) {
return amountDelta;
}
return left.axis_value.localeCompare(right.axis_value, "ru");
})
.slice(0, 5);
return {
value_flow_direction: input.direction,
ranking_need: input.rankingNeed,
ranking_axis: "counterparty",
organization_scope: input.organizationScope,
period_scope: input.periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
ranked_values: rankedValues,
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,
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
};
}
function deriveValueFlowSideSummary(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
): AssistantMcpDiscoveryValueFlowSideSummary {
@@ -1798,6 +1918,18 @@ function buildValueFlowConfirmedFacts(
];
}
function buildRankedValueFlowConfirmedFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
if (!derived || derived.ranked_values.length <= 0) {
return [];
}
const leader = derived.ranked_values[0];
const directionLabel =
derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
return [
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
];
}
function buildBidirectionalValueFlowConfirmedFacts(
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
@@ -1880,6 +2012,19 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
return facts;
}
function buildRankedValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
if (!derived) {
return [];
}
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
facts.push(
"Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit"
);
}
return facts;
}
function buildBidirectionalValueFlowInferredFacts(
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
@@ -1939,6 +2084,22 @@ function buildValueFlowUnknownFacts(
return unknownFacts;
}
function buildRankedValueFlowUnknownFacts(
periodScope: string | null,
derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null
): string[] {
const unknownFacts: string[] = [];
if (derived?.coverage_limited_by_probe_limit) {
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
}
unknownFacts.push(
periodScope
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time counterparty ranking is not proven without an explicit checked period"
);
return unknownFacts;
}
function buildBidirectionalValueFlowUnknownFacts(
periodScope: string | null,
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
@@ -1979,6 +2140,8 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As
return "metadata_inspection_v1";
case "movement_evidence":
return "counterparty_movement_evidence_query_movements_v1";
case "value_flow_comparison":
case "value_flow_ranking":
case "value_flow":
return valueFlowPilotProfile(planner).scope;
case "document_evidence":
@@ -2107,7 +2270,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const organizationScope = organizationScopeForPlanner(planner);
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (metadataPilotEligible) {
let metadataResult: AddressMcpMetadataRowsResult | null = null;
@@ -2694,6 +2859,50 @@ export async function executeAssistantMcpDiscoveryPilot(
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
organizationScope,
periodScope: dateScope,
direction: valueFlowProfile.direction,
rankingNeed
});
if (derivedRankedValueFlow) {
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: valueFlowProfile.scope,
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: derivedRankedValueFlow,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
const derivedValueFlow = deriveValueFlow(
queryResult,
counterparty,
@@ -2733,6 +2942,7 @@ export async function executeAssistantMcpDiscoveryPilot(
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
@@ -8,6 +8,7 @@ import {
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
type AssistantMcpCatalogPlanReview
} from "./assistantMcpCatalogIndex";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
@@ -17,6 +18,8 @@ export type AssistantMcpDiscoveryChainId =
| "metadata_inspection"
| "metadata_lane_clarification"
| "value_flow"
| "value_flow_comparison"
| "value_flow_ranking"
| "lifecycle"
| "movement_evidence"
| "document_evidence"
@@ -24,6 +27,7 @@ export type AssistantMcpDiscoveryChainId =
export interface AssistantMcpDiscoveryPlannerInput {
semanticDataNeed?: string | null;
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
}
@@ -32,6 +36,7 @@ export interface AssistantMcpDiscoveryPlannerContract {
policy_owner: "assistantMcpDiscoveryPlanner";
planner_status: AssistantMcpDiscoveryPlannerStatus;
semantic_data_need: string | null;
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
selected_chain_id: AssistantMcpDiscoveryChainId;
selected_chain_summary: string;
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
@@ -93,6 +98,10 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined): boolean {
return (graph?.subject_candidates.length ?? 0) > 0;
}
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
@@ -136,14 +145,150 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const meaning = input.turnMeaning ?? null;
const dataNeedGraph = input.dataNeedGraph ?? null;
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
chainId: "metadata_lane_clarification",
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
};
}
if (graphFactFamily === "value_flow") {
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "bidirectional value-flow comparison evidence",
chainId: "value_flow_comparison",
chainSummary:
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
primitives: ["query_movements", "probe_coverage"],
axes,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"
};
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "ranked value-flow evidence",
chainId: "value_flow_ranking",
chainSummary:
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason:
dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph"
};
}
if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis");
return {
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_from_data_need_graph"
};
}
if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_from_data_need_graph"
};
}
if (graphFactFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_from_data_need_graph"
};
}
if (graphFactFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_from_data_need_graph"
};
}
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason:
graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe"
};
}
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
return {
@@ -267,8 +412,12 @@ export function planAssistantMcpDiscovery(
const recipe = recipeFor(input);
const budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const dataNeedGraph = input.dataNeedGraph ?? null;
const reasonCodes: string[] = [];
pushReason(reasonCodes, recipe.reason);
if (dataNeedGraph) {
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
}
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
@@ -296,6 +445,7 @@ export function planAssistantMcpDiscovery(
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
data_need_graph: dataNeedGraph,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
@@ -91,6 +91,36 @@ function userFacingLines(values: string[]): string[] {
}
function localizeLine(value: string): string {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности в запрошенном срезе.";
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
}
const openScopeBidirectionalMatch = value.match(
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
);
if (openScopeBidirectionalMatch) {
const incoming =
openScopeBidirectionalMatch[1] === "found"
? "входящие строки найдены"
: "входящие строки не найдены";
const outgoing =
openScopeBidirectionalMatch[2] === "found"
? "исходящие строки найдены"
: "исходящие строки не найдены";
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
}
if (
/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(
value
)
) {
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
}
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) {
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;
@@ -11,6 +11,7 @@ import {
planAssistantMcpDiscovery,
type AssistantMcpDiscoveryPlannerContract
} from "./assistantMcpDiscoveryPlanner";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
@@ -25,6 +26,7 @@ export type AssistantMcpDiscoveryRuntimeBridgeStatus =
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
semanticDataNeed?: string | null;
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
}
@@ -98,6 +100,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
const planner = planAssistantMcpDiscovery({
semanticDataNeed: input.semanticDataNeed,
dataNeedGraph: input.dataNeedGraph,
turnMeaning: input.turnMeaning
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
@@ -101,6 +101,7 @@ export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
semanticDataNeed: turnInput.semantic_data_need,
dataNeedGraph: turnInput.data_need_graph,
turnMeaning: turnInput.turn_meaning_ref,
deps: input.deps
});
@@ -1,4 +1,8 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
import {
buildAssistantMcpDiscoveryDataNeedGraph,
type AssistantMcpDiscoveryDataNeedGraphContract
} from "./assistantMcpDiscoveryDataNeedGraph";
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
"assistant_mcp_discovery_turn_input_v1" as const;
@@ -25,6 +29,7 @@ export interface AssistantMcpDiscoveryTurnInputContract {
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
should_run_discovery: boolean;
semantic_data_need: string | null;
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
source_signal: AssistantMcpDiscoveryTurnInputSource;
reason_codes: string[];
@@ -114,6 +119,10 @@ function compactLower(value: unknown): string {
.trim();
}
function sameScopedName(left: string | null, right: string | null): boolean {
return Boolean(left && right && compactLower(left) === compactLower(right));
}
function candidateValue(value: unknown): string | null {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
@@ -407,6 +416,12 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
);
}
function hasValueRankingSignal(text: string): boolean {
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(
text
);
}
function hasMonthlyAggregationSignal(text: string): boolean {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
text
@@ -741,7 +756,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal =
!rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
!rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawEntityResolutionSignal =
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
@@ -773,6 +789,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
predecomposeEntities.counterparty,
predecomposeEntities.organization
);
const organizationMirrorsPredecomposeCounterparty = Boolean(
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty)
);
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
? null
: predecomposeEntities.counterparty;
const predecomposeDateScope = collectDateScope(predecomposeContract);
const followupDiscoverySeedApplicable = Boolean(
followupSeed.domain &&
@@ -1038,7 +1066,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
}
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
} else {
if (groundedFollowupEntity) {
@@ -1047,7 +1075,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
}
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
if (!groundedFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
@@ -1058,14 +1086,26 @@ export function buildAssistantMcpDiscoveryTurnInput(
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
const openScopeValueFlowWithoutCounterparty =
valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope =
openScopeValueFlowWithoutCounterparty &&
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
}
const explicitOrganizationScope =
valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
? null
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
: null;
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
if (entityCandidates[index] === explicitOrganizationScope) {
entityCandidates.splice(index, 1);
}
}
}
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
@@ -1312,7 +1352,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
if (
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty
) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (followupSeed.counterparty) {
@@ -1330,6 +1373,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
const dataNeedGraph =
runDiscovery && hasTurnMeaning
? buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed,
rawUtterance: rawSignalSourceText,
turnMeaning: cleanTurnMeaning
})
: null;
if (dataNeedGraph) {
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
@@ -1337,6 +1391,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
data_need_graph: dataNeedGraph,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes
@@ -222,6 +222,74 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("asks for organization rather than counterparty when a ranked value-flow ask already has the period", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("ranking");
expect(draft.next_step_line).toContain("организацию");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("asks for organization rather than counterparty on open bidirectional comparison when only the period is known", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("входящий и исходящий");
expect(draft.next_step_line).toContain("организацию");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
describe("assistant MCP discovery data need graph", () => {
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "какое нетто по деньгам с SVK за 2020 год по месяцам",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
asked_aggregation_axis: "month",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.action_family).toBe("net_value_flow");
expect(result.aggregation_need).toBe("by_month");
expect(result.time_scope_need).toBe("explicit_period");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.proof_expectation).toBe("coverage_checked_fact");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"resolve_entity_reference",
"collect_incoming_movements",
"collect_outgoing_movements",
"aggregate_by_month",
"probe_coverage"
]);
expect(result.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
});
it("marks metadata lane choice as a clarification-required graph", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "metadata lane clarification",
rawUtterance: "давай дальше",
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
});
expect(result.business_fact_family).toBe("schema_surface");
expect(result.clarification_gaps).toEqual(["lane_family_choice"]);
expect(result.proof_expectation).toBe("clarification_required");
});
it("keeps entity search as an entity-grounding graph", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "entity discovery evidence",
rawUtterance: "найди в 1С контрагента Группа СВК",
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"]
}
});
expect(result.business_fact_family).toBe("entity_grounding");
expect(result.subject_candidates).toEqual(["Группа СВК"]);
expect(result.proof_expectation).toBe("entity_grounding");
expect(result.decomposition_candidates).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.forbidden_overclaim_flags).toContain("no_unresolved_entity_claim");
});
it("treats top-value wording as a ranking ask rather than a missing-subject fact ask", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "кто больше всего принес денег в 2020",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.ranking_need).toBe("top_desc");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_ranked_axis_values",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_ranking_top_desc");
});
it("treats incoming-vs-outgoing comparison as an open-scope value need rather than a missing-subject fact ask", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "что больше: входящие или исходящие деньги за 2020 год?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_incoming_movements",
"collect_outgoing_movements",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
});
});
@@ -4,6 +4,27 @@ import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscovery
describe("assistant MCP discovery planner", () => {
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [
"resolve_entity_reference",
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
@@ -25,8 +46,10 @@ 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.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
expect(result.reason_codes).toContain("planner_consumed_data_need_graph_v1");
});
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
@@ -91,6 +114,22 @@ describe("assistant MCP discovery planner", () => {
it("builds a movement discovery plan without aggregating value-flow totals", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["resolve_entity_reference", "fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
@@ -107,7 +146,192 @@ describe("assistant MCP discovery planner", () => {
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_movement_recipe");
expect(result.reason_codes).toContain("planner_selected_movement_from_data_need_graph");
});
it("can select value-flow chain from data need graph even when turn meaning family is still under-specified", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: "by_month",
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [
"resolve_entity_reference",
"collect_incoming_movements",
"collect_outgoing_movements",
"aggregate_by_month",
"probe_coverage"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
asked_aggregation_axis: "month"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(result.required_axes).toEqual([
"counterparty",
"period",
"aggregate_axis",
"amount",
"coverage_target",
"calendar_month"
]);
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_from_data_need_graph");
});
it("does not collapse a ranking-shaped value graph into entity-resolution just because no subject is preselected", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps ranked value-flow ready for execution once checked period and organization are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
});
it("does not collapse incoming-vs-outgoing comparison into entity-resolution when no counterparty is preselected", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow_comparison");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "amount", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps bidirectional comparison ready for execution once checked period and organization are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_comparison");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
@@ -397,4 +397,41 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).toBeNull();
expect(candidate.eligible_for_future_hot_runtime).toBe(false);
});
it("localizes open-scope bidirectional comparison scope and probe-limit wording without contour garbage", () => {
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:
"\u041f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0445 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0439; \u043d\u0435\u0442\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u0430\u043a \u0440\u0430\u0441\u0447\u0435\u0442 \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u0435\u0440\u0438\u043e\u0434\u0443.",
confirmed_lines: [
"1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=found, outgoing=found"
],
inference_lines: [],
unknown_lines: [],
limitation_lines: [
"Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count"
],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain(
"\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435"
);
expect(candidate.reply_text).toContain(
"\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP"
);
expect(candidate.reply_text).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443"
);
expect(candidate.reply_text).not.toContain("Requested period hit the MCP row limit");
});
});
@@ -13,6 +13,30 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
};
}
function buildBidirectionalDeps(
incomingRows: Array<Record<string, unknown>>,
outgoingRows: Array<Record<string, unknown>>
) {
return {
executeAddressMcpQuery: vi
.fn()
.mockResolvedValueOnce({
fetched_rows: incomingRows.length,
matched_rows: incomingRows.length,
raw_rows: incomingRows,
rows: incomingRows,
error: null
})
.mockResolvedValueOnce({
fetched_rows: outgoingRows.length,
matched_rows: outgoingRows.length,
raw_rows: outgoingRows,
rows: outgoingRows,
error: null
})
};
}
describe("assistant MCP discovery runtime bridge", () => {
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
@@ -51,6 +75,166 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
});
it("keeps ranked value-flow in clarification without asking for a counterparty when only the period is known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.answer_draft.headline).toContain("ranking");
expect(result.answer_draft.next_step_line).toContain("организацию");
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 1200, Counterparty: "СВК-А" },
{ Period: "2020-03-11T00:00:00", Amount: 800, Counterparty: "СВК-Б" },
{ Period: "2020-05-12T00:00:00", Amount: 900, Counterparty: "СВК-А" }
])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.pilot.derived_ranked_value_flow?.ranked_values[0]).toMatchObject({
axis_value: "СВК-А",
total_amount: 2100
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
});
it("keeps open bidirectional comparison in clarification without asking for a counterparty when only the period is known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.answer_draft.headline).toContain("входящий и исходящий");
expect(result.answer_draft.next_step_line).toContain("организацию");
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("produces a bounded bidirectional comparison answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildBidirectionalDeps(
[
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "СВК-А" },
{ Period: "2020-04-11T00:00:00", Amount: 1800, Counterparty: "СВК-Б" }
],
[{ Period: "2020-02-12T00:00:00", Amount: 1400, Counterparty: "СВК-А" }]
)
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
period_scope: "2020",
incoming_customer_revenue: {
total_amount: 5000
},
outgoing_supplier_payout: {
total_amount: 1400
}
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("получили");
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
});
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
@@ -13,6 +13,30 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
};
}
function buildBidirectionalDeps(
incomingRows: Array<Record<string, unknown>>,
outgoingRows: Array<Record<string, unknown>>
) {
return {
executeAddressMcpQuery: vi
.fn()
.mockResolvedValueOnce({
fetched_rows: incomingRows.length,
matched_rows: incomingRows.length,
raw_rows: incomingRows,
rows: incomingRows,
error: null
})
.mockResolvedValueOnce({
fetched_rows: outgoingRows.length,
matched_rows: outgoingRows.length,
raw_rows: outgoingRows,
rows: outgoingRows,
error: null
})
};
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
@@ -240,4 +264,93 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
});
it("runs raw incoming-vs-outgoing comparison as an open-scope value-flow chain without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildBidirectionalDeps(
[
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
],
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }]
)
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("получили");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
});
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: "ООО Альтернатива Плюс",
organization: "ООО Альтернатива Плюс"
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildBidirectionalDeps(
[{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" }],
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }]
)
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
});
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison (utf8-safe)", async () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: orgName,
organization: orgName
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildBidirectionalDeps(
[{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0410" }],
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "\u041f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a-\u0410" }]
)
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
});
});
@@ -21,6 +21,8 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.time_scope_need).toBe("explicit_period");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
@@ -160,6 +162,8 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("raw_text");
expect(result.semantic_data_need).toBe("1C metadata evidence");
expect(result.data_need_graph?.business_fact_family).toBe("schema_surface");
expect(result.data_need_graph?.decomposition_candidates).toEqual(["inspect_metadata_surface"]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_fields",
@@ -198,6 +202,8 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("raw_text");
expect(result.semantic_data_need).toBe("entity discovery evidence");
expect(result.data_need_graph?.business_fact_family).toBe("entity_grounding");
expect(result.data_need_graph?.subject_candidates).toEqual(["Группа СВК"]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
@@ -1256,4 +1262,119 @@ describe("assistant MCP discovery turn input adapter", () => {
"\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u043d\u0430\u0439\u0442\u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c '\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a'"
);
});
it("marks top-value wording as a ranking data need without inventing a missing subject gap", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "кто больше всего принес денег в 2020"
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.data_need_graph?.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_ranked_axis_values",
"probe_coverage"
]);
});
it("keeps organization as scope for open bidirectional comparison wording instead of inventing a subject candidate", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("drops organization-shaped assistant-turn entity pollution when open comparison already has explicit organization scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: [{ value: "ООО Альтернатива Плюс" }],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
},
predecomposeContract: {
entities: { counterparty: "ООО Альтернатива Плюс", organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
});
it.skip("treats mirrored predecompose organization and counterparty as organization scope for open comparison", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: "ООО Альтернатива Плюс",
organization: "ООО Альтернатива Плюс"
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it.skip("treats mirrored predecompose organization and counterparty as organization scope for open comparison (utf8-safe)", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: orgName,
organization: orgName
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
});