ARCH: добавить value-flow pilot MCP discovery

This commit is contained in:
dctouch 2026-04-20 18:15:26 +03:00
parent e85d456576
commit 49fd08652c
21 changed files with 1100 additions and 66 deletions

View File

@ -1091,6 +1091,69 @@ Validation:
- `npm run build` passed;
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun3 --timeout-seconds 180` passed 4/4, final status `accepted`.
## Progress Update - 2026-04-20 MCP Discovery Value-Flow Pilot And Deep Response Gate
The sixteenth implementation slice of Big Block 5 extends guarded MCP discovery from lifecycle evidence to the first bounded value-flow evidence path:
- `assistantMcpDiscoveryTurnInputAdapter.ts`
- `assistantMcpDiscoveryPilotExecutor.ts`
- `assistantMcpDiscoveryAnswerAdapter.ts`
- `assistantMcpDiscoveryResponseCandidate.ts`
- `assistantMcpDiscoveryResponsePolicy.ts`
- `assistantDeepTurnResponseRuntimeAdapter.ts`
- `address_truth_harness_phase19_mcp_discovery_response_gate.json`
The new path is deliberately narrow.
It does not turn the assistant into a free-form autonomous 1C agent yet. It adds a guarded pilot for counterparty value-flow questions when the exact route does not own the current turn:
- raw Russian/English value-flow signals such as `денежный поток`, `оборот`, `выручка`, `оплата`, `turnover`, and `revenue`;
- Cyrillic-safe signal detection instead of JavaScript `\w` assumptions;
- organization-shaped targets can be reinterpreted as counterparty candidates for this contour when no explicit counterparty was extracted;
- the planner may choose value-flow primitives, but the pilot executes only the guarded `query_movements` branch through the existing `customer_revenue_and_payments` recipe;
- the pilot derives `derived_value_flow` from confirmed movement rows: counterparty, period scope, matched rows, rows with amount, total amount, first movement date, latest movement date, and `inference_basis=sum_of_confirmed_1c_value_flow_rows`;
- the user-facing answer says what was found, what was calculated, and what is not proven outside the checked period;
- internal terms such as primitive names, query ids, runtime/planner/catalog mechanics, and `pilot_` reason codes are filtered from the final answer.
The deep response adapter now preserves the MCP discovery entry point in deep runtime meta and can replace a bad deep/partial answer with a guarded discovery candidate only when:
- the entry point is `bridge_executed`;
- discovery was attempted;
- `turn_input.should_run_discovery=true`;
- the current reply source is `deep_analysis`, `partial_coverage`, or `normalizer_v2_0_2`;
- the response candidate passes the same guarded text checks as the living-chat gate.
Live replay finding:
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun4` failed after adding the value-flow question because the visible answer still came from the deep partial path and drifted into unrelated store/asset/amortization wording;
- after preserving the discovery entry point for deep packaging and allowing the guarded deep candidate, `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun5` passed 5/5;
- after the punctuation cleanup, `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun6` also passed 5/5, final status `accepted`.
The value-flow replay answer for `какой денежный поток был у Группа СВК за 2020 год?` now states:
- found 1C money-movement rows for `Группа СВК`;
- period: `2020`;
- calculated sum: `47 628 853,03 руб.`;
- rows with amount: `44 из 44`;
- first movement date: `2020-01-09`;
- latest movement date: `2020-12-30`;
- full turnover outside the checked period is not confirmed by this search.
Validation:
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 37/37;
- `npm run build` passed;
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun6 --timeout-seconds 180` passed 5/5, final status `accepted`;
- step-level human answer review confirmed no `query_documents`, `query_movements`, `runtime_`, `planner_`, `catalog_`, `primitive`, or `pilot_` leak in lifecycle/value-flow user-facing answers.
Known remaining boundary:
- this slice covers incoming customer value-flow through `customer_revenue_and_payments`; supplier payouts, bidirectional money flow, arbitrary cross-register discovery, and multi-axis aggregation are still future MCP semantic discovery work.
Module progress:
- Big Block 5 MCP Semantic Data Agent: `97%`.
## Execution Rule
Do not implement this plan as:

View File

@ -82,7 +82,36 @@
]
},
{
"step_id": "step_04_off_domain_living_chat_not_hijacked",
"step_id": "step_04_counterparty_value_flow_uses_guarded_discovery",
"title": "Unsupported-but-understood counterparty value-flow question uses guarded discovery answer",
"question": "какой денежный поток был у Группа СВК за 2020 год?",
"required_answer_patterns_all": [
"(?i)свк",
"(?i)1с|найден|строк|подтвержд",
"(?i)денеж|оборот|сумм|руб",
"(?i)2020|период",
"(?i)не подтвержд|проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)точный маршрут.*не подключ",
"(?i)не буду подставлять",
"(?i)query_documents",
"(?i)query_movements",
"(?i)runtime_",
"(?i)planner_",
"(?i)catalog_",
"(?i)primitive",
"(?i)pilot_"
],
"criticality": "critical",
"semantic_tags": [
"mcp_discovery_value_flow",
"counterparty_turnover",
"unsupported_current_turn_meaning_boundary"
]
},
{
"step_id": "step_05_off_domain_living_chat_not_hijacked",
"title": "Off-domain living chat remains human and is not hijacked by discovery carryover",
"question": "а чем капибара отличается от утки?",
"required_answer_patterns_any": [

View File

@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantDeepTurnResponseRuntime = runAssistantDeepTurnResponseRuntime;
const assistantDeepTurnPackagingRuntimeAdapter_1 = require("./assistantDeepTurnPackagingRuntimeAdapter");
const assistantDeepTurnFinalizeRuntimeAdapter_1 = require("./assistantDeepTurnFinalizeRuntimeAdapter");
const assistantMcpDiscoveryResponsePolicy_1 = require("./assistantMcpDiscoveryResponsePolicy");
function toNullableString(value) {
if (typeof value !== "string") {
return null;
@ -68,12 +69,15 @@ function normalizeAddressRuntimeMetaForDeep(value) {
toolGateDecision: toNullableString(source.toolGateDecision),
toolGateReason: toNullableString(source.toolGateReason),
predecomposeContract: source.predecomposeContract ?? null,
orchestrationContract: source.orchestrationContract ?? null
semanticExtractionContract: source.semanticExtractionContract ?? null,
orchestrationContract: source.orchestrationContract ?? null,
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
};
}
function runAssistantDeepTurnResponseRuntime(input) {
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? assistantDeepTurnPackagingRuntimeAdapter_1.runAssistantDeepTurnPackagingRuntime;
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? assistantDeepTurnFinalizeRuntimeAdapter_1.finalizeAssistantDeepTurn;
const addressRuntimeMetaForDeep = normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep);
const packagingRuntime = runPackagingRuntimeSafe({
featureInvestigationStateV1: input.featureInvestigationStateV1,
sessionId: input.sessionId,
@ -108,7 +112,7 @@ function runAssistantDeepTurnResponseRuntime(input) {
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
previousInvestigationState: input.previousInvestigationState ?? null,
addressRuntimeMetaForDeep: normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep),
addressRuntimeMetaForDeep,
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState,
@ -116,12 +120,35 @@ function runAssistantDeepTurnResponseRuntime(input) {
persistInvestigationState: input.persistInvestigationState,
messageIdFactory: input.messageIdFactory
});
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
currentReply: packagingRuntime.safeAssistantReply,
currentReplySource: "deep_analysis",
addressRuntimeMeta: addressRuntimeMetaForDeep
});
const assistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: packagingRuntime.safeAssistantReply;
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
const debug = {
...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
};
const assistantItem = mcpDiscoveryResponsePolicy.applied
? {
...packagingRuntime.assistantItem,
text: assistantReply,
reply_type: replyType,
debug
}
: packagingRuntime.assistantItem;
const finalization = runFinalizeDeepTurnSafe({
sessionId: input.sessionId,
assistantReply: packagingRuntime.safeAssistantReply,
replyType: input.composition.reply_type,
assistantItem: packagingRuntime.assistantItem,
debug: packagingRuntime.debug,
assistantReply,
replyType,
assistantItem,
debug,
deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails,
appendItem: input.appendItem,
getSession: input.getSession,
@ -131,6 +158,6 @@ function runAssistantDeepTurnResponseRuntime(input) {
});
return {
response: finalization.response,
debug: packagingRuntime.debug
debug
};
}

View File

@ -36,6 +36,7 @@ function isInternalMechanicsLine(value) {
text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") ||
text.includes("pilot_") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_"));
@ -58,7 +59,10 @@ function modeFor(pilot) {
}
return "checked_sources_only";
}
function headlineFor(mode) {
function headlineFor(mode, pilot) {
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
@ -87,11 +91,17 @@ function nextStepFor(mode, pilot) {
}
function buildMustNotClaim(pilot) {
const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false."
];
if (pilot.pilot_scope === "counterparty_lifecycle_query_documents_v1") {
claims.push("Do not claim legal registration age unless a legal registration source is confirmed.");
claims.push("Do not present inferred activity duration as a formally confirmed legal fact.");
}
if (pilot.pilot_scope === "counterparty_value_flow_query_movements_v1") {
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.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
@ -108,6 +118,18 @@ function derivedActivityInferenceLine(pilot) {
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
].join(" ");
}
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
return null;
}
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const dates = flow.first_movement_date && flow.latest_movement_date
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
: "";
return `По найденным строкам денежных движений в 1С${counterparty}${period} сумма составляет ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates} Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.`;
}
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
@ -122,12 +144,16 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode,
headline: headlineFor(mode),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),

View File

@ -81,6 +81,19 @@ function buildLifecycleFilters(planner) {
sort: "period_asc"
};
}
function buildValueFlowFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
@ -89,6 +102,19 @@ function isLifecyclePilotEligible(planner) {
return (planner.proposed_primitives.includes("query_documents") &&
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age")));
}
function isValueFlowPilotEligible(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported}`;
return (planner.proposed_primitives.includes("query_movements") &&
(combined.includes("turnover") ||
combined.includes("revenue") ||
combined.includes("payment") ||
combined.includes("payout") ||
combined.includes("value")));
}
function skippedProbeResult(step, limitation) {
return {
primitive_id: step.primitive_id,
@ -107,7 +133,7 @@ function queryResultToProbeResult(primitiveId, result) {
limitation: result.error
};
}
function summarizeRows(result) {
function summarizeLifecycleRows(result) {
if (result.error) {
return null;
}
@ -116,6 +142,15 @@ function summarizeRows(result) {
}
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
}
function summarizeValueFlowRows(result) {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP value-flow rows fetched";
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function rowDateValue(row) {
const candidates = [
row["Период"],
@ -134,6 +169,37 @@ function rowDateValue(row) {
}
return null;
}
function rowAmountValue(row) {
const candidates = [
row["Сумма"],
row["РЎСѓРјРјР°"],
row["СуммаДокумента"],
row["СуммаДокумента"],
row["Amount"],
row["amount"],
row["Total"],
row["total"]
];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate)) {
return candidate;
}
const text = toNonEmptyString(candidate);
if (!text) {
continue;
}
const normalized = text
.replace(/\s+/g, "")
.replace(/\u00a0/g, "")
.replace(",", ".")
.replace(/[^\d.-]/g, "");
const parsed = Number(normalized);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function monthDiff(firstIsoDate, latestIsoDate) {
const first = new Date(`${firstIsoDate}T00:00:00.000Z`);
const latest = new Date(`${latestIsoDate}T00:00:00.000Z`);
@ -184,7 +250,48 @@ function deriveActivityPeriod(result) {
inference_basis: "first_and_latest_confirmed_1c_activity_rows"
};
}
function buildConfirmedFacts(result, counterparty) {
function formatAmountHumanRu(amount) {
const formatted = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 2,
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2
})
.format(amount)
.replace(/\u00a0/g, " ");
return `${formatted} руб.`;
}
function deriveValueFlow(result, counterparty, periodScope) {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
const amount = rowAmountValue(row);
if (amount !== null) {
totalAmount += amount;
rowsWithAmount += 1;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const dates = result.rows
.map((row) => rowDateValue(row))
.filter((value) => Boolean(value))
.sort();
return {
counterparty,
period_scope: periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
total_amount: totalAmount,
total_amount_human_ru: formatAmountHumanRu(totalAmount),
first_movement_date: dates[0] ?? null,
latest_movement_date: dates[dates.length - 1] ?? null,
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function buildLifecycleConfirmedFacts(result, counterparty) {
if (result.error || result.matched_rows <= 0) {
return [];
}
@ -194,15 +301,38 @@ function buildConfirmedFacts(result, counterparty) {
: "1C activity rows were found for the requested counterparty scope"
];
}
function buildInferredFacts(result) {
function buildValueFlowConfirmedFacts(result, counterparty) {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildLifecycleInferredFacts(result) {
if (result.error || result.fetched_rows <= 0) {
return [];
}
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
}
function buildUnknownFacts() {
function buildValueFlowInferredFacts(derived) {
if (!derived) {
return [];
}
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
}
function buildLifecycleUnknownFacts() {
return ["Legal registration date is not proven by this MCP discovery pilot"];
}
function buildValueFlowUnknownFacts(periodScope) {
return [
periodScope
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time turnover is not proven without an explicit checked period"
];
}
function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
@ -235,6 +365,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes
};
@ -255,11 +386,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes
};
}
if (!isLifecyclePilotEligible(planner)) {
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@ -279,12 +413,94 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes
};
}
let queryResult = null;
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
if (valueFlowPilotEligible) {
let queryResult = null;
const filters = buildValueFlowFilters(planner);
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("customer_revenue_and_payments", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Value-flow recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_movements") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_value_flow_uses_query_movements_and_derives_aggregate"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
unknownFacts: buildValueFlowUnknownFacts(dateScope),
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: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
let queryResult = null;
const filters = buildLifecycleFilters(planner);
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) {
@ -303,6 +519,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes
};
@ -329,7 +546,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
const sourceRowsSummary = queryResult ? summarizeLifecycleRows(queryResult) : null;
const derivedActivityPeriod = deriveActivityPeriod(queryResult);
if (derivedActivityPeriod) {
pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows");
@ -337,9 +554,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(),
confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
unknownFacts: buildLifecycleUnknownFacts(),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
@ -357,6 +574,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};

View File

@ -51,6 +51,7 @@ function hasInternalMechanics(value) {
return (text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_") ||
@ -67,12 +68,28 @@ function localizeLine(value) {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности по запрошенному контрагентскому контуру.";
}
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
if (valueFlowMatch) {
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
}
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
}
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена.";
}
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
}
return value;
}
function section(title, lines) {

View File

@ -41,6 +41,7 @@ function hasInternalMechanics(value) {
return (text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_") ||
@ -72,6 +73,14 @@ function isDiscoveryReadyChatCandidate(input, entryPoint) {
turnInput?.should_run_discovery === true &&
(input.livingChatSource === "llm_chat" || input.currentReplySource === "llm_chat"));
}
function isDiscoveryReadyDeepCandidate(input, entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
return (entryPoint?.entry_status === "bridge_executed" &&
entryPoint.discovery_attempted === true &&
turnInput?.should_run_discovery === true &&
(source === "deep_analysis" || source === "partial_coverage" || source === "normalizer_v2_0_2"));
}
function applyAssistantMcpDiscoveryResponsePolicy(input) {
const currentReply = String(input.currentReply ?? "");
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
@ -80,6 +89,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
}
@ -89,6 +99,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (!discoveryReadyChatCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
}
if (!discoveryReadyDeepCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@ -102,7 +115,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
}
const canApply = Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -95,12 +95,15 @@ function collectDateScope(predecompose) {
function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
}
function hasValueFlowSignal(text) {
return /(?:оборот|выручк|оплат|плат[её]ж|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|value[-\s]?flow|turnover|revenue|payment|payout|cash\s+flow)/iu.test(text);
}
function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
if (/(?:document|documents|list_documents)/iu.test(combined)) {
@ -115,6 +118,9 @@ function shouldRunDiscovery(input) {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
@ -127,6 +133,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const reasonCodes = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
@ -135,18 +142,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
domain: rawDomain,
action: rawAction,
unsupported,
lifecycleSignal
lifecycleSignal,
valueFlowSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization);
}
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
const turnMeaning = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value_or_turnover" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
};
const cleanTurnMeaning = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
@ -173,6 +185,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const runDiscovery = shouldRunDiscovery({
unsupported,
lifecycleSignal,
valueFlowSignal,
semanticDataNeed,
explicitIntentCandidate
});
@ -183,10 +196,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "predecompose_contract"
: lifecycleSignal
? "raw_text"
: "none";
: valueFlowSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
if (valueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}

View File

@ -20,6 +20,7 @@ import {
finalizeAssistantDeepTurn,
type FinalizeAssistantDeepTurnInput
} from "./assistantDeepTurnFinalizeRuntimeAdapter";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence";
import type { CompanyAnchorSet } from "./companyAnchorResolver";
import type {
@ -181,7 +182,9 @@ function normalizeAddressRuntimeMetaForDeep(
toolGateDecision: toNullableString(source.toolGateDecision),
toolGateReason: toNullableString(source.toolGateReason),
predecomposeContract: source.predecomposeContract ?? null,
orchestrationContract: source.orchestrationContract ?? null
semanticExtractionContract: source.semanticExtractionContract ?? null,
orchestrationContract: source.orchestrationContract ?? null,
mcpDiscoveryRuntimeEntryPoint: source.mcpDiscoveryRuntimeEntryPoint ?? null
};
}
@ -190,6 +193,7 @@ export function runAssistantDeepTurnResponseRuntime(
): RunAssistantDeepTurnResponseRuntimeOutput {
const runPackagingRuntimeSafe = input.runPackagingRuntime ?? runAssistantDeepTurnPackagingRuntime;
const runFinalizeDeepTurnSafe = input.runFinalizeDeepTurn ?? finalizeAssistantDeepTurn;
const addressRuntimeMetaForDeep = normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep);
const packagingRuntime = runPackagingRuntimeSafe({
featureInvestigationStateV1: input.featureInvestigationStateV1,
@ -225,7 +229,7 @@ export function runAssistantDeepTurnResponseRuntime(
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
previousInvestigationState: input.previousInvestigationState ?? null,
addressRuntimeMetaForDeep: normalizeAddressRuntimeMetaForDeep(input.addressRuntimeMetaForDeep),
addressRuntimeMetaForDeep,
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState,
@ -234,12 +238,36 @@ export function runAssistantDeepTurnResponseRuntime(
messageIdFactory: input.messageIdFactory
});
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: packagingRuntime.safeAssistantReply,
currentReplySource: "deep_analysis",
addressRuntimeMeta: addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null
});
const assistantReply = mcpDiscoveryResponsePolicy.applied
? mcpDiscoveryResponsePolicy.reply_text
: packagingRuntime.safeAssistantReply;
const replyType = mcpDiscoveryResponsePolicy.applied ? "partial_coverage" : input.composition.reply_type;
const debug = {
...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
} as AssistantDebugPayload;
const assistantItem = mcpDiscoveryResponsePolicy.applied
? {
...packagingRuntime.assistantItem,
text: assistantReply,
reply_type: replyType,
debug
}
: packagingRuntime.assistantItem;
const finalization = runFinalizeDeepTurnSafe({
sessionId: input.sessionId,
assistantReply: packagingRuntime.safeAssistantReply,
replyType: input.composition.reply_type,
assistantItem: packagingRuntime.assistantItem,
debug: packagingRuntime.debug,
assistantReply,
replyType,
assistantItem,
debug,
deepAnalysisLogDetails: packagingRuntime.deepAnalysisLogDetails,
appendItem: input.appendItem,
getSession: input.getSession,
@ -250,6 +278,6 @@ export function runAssistantDeepTurnResponseRuntime(
return {
response: finalization.response,
debug: packagingRuntime.debug
debug
};
}

View File

@ -62,6 +62,7 @@ function isInternalMechanicsLine(value: string): boolean {
text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") ||
text.includes("pilot_") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_")
@ -88,7 +89,10 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
return "checked_sources_only";
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode): string {
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
@ -119,11 +123,17 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false."
];
if (pilot.pilot_scope === "counterparty_lifecycle_query_documents_v1") {
claims.push("Do not claim legal registration age unless a legal registration source is confirmed.");
claims.push("Do not present inferred activity duration as a formally confirmed legal fact.");
}
if (pilot.pilot_scope === "counterparty_value_flow_query_movements_v1") {
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.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
@ -142,6 +152,20 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
].join(" ");
}
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_value_flow;
if (!flow) {
return null;
}
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const dates =
flow.first_movement_date && flow.latest_movement_date
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
: "";
return `По найденным строкам денежных движений в 1С${counterparty}${period} сумма составляет ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates} Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.`;
}
export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract {
@ -158,13 +182,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedValueLine = derivedValueFlowConfirmedLine(pilot);
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode,
headline: headlineFor(mode),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
headline: headlineFor(mode, pilot),
confirmed_lines: uniqueStrings(confirmedLines),
inference_lines: uniqueStrings(inferenceLines),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),

View File

@ -40,11 +40,27 @@ export interface AssistantMcpDiscoveryDerivedActivityPeriod {
inference_basis: "first_and_latest_confirmed_1c_activity_rows";
}
export interface AssistantMcpDiscoveryDerivedValueFlow {
counterparty: string | null;
period_scope: string | null;
rows_matched: number;
rows_with_amount: number;
total_amount: number;
total_amount_human_ru: string;
first_movement_date: string | null;
latest_movement_date: string | null;
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
}
export type AssistantMcpDiscoveryPilotScope =
| "counterparty_lifecycle_query_documents_v1"
| "counterparty_value_flow_query_movements_v1";
export interface AssistantMcpDiscoveryPilotExecutionContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryPilotExecutor";
pilot_status: AssistantMcpDiscoveryPilotStatus;
pilot_scope: "counterparty_lifecycle_query_documents_v1";
pilot_scope: AssistantMcpDiscoveryPilotScope;
dry_run: AssistantMcpDiscoveryRuntimeDryRunContract;
mcp_execution_performed: boolean;
executed_primitives: string[];
@ -53,6 +69,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
evidence: AssistantMcpDiscoveryEvidenceContract;
source_rows_summary: string | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
query_limitations: string[];
reason_codes: string[];
}
@ -141,6 +158,20 @@ function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): A
};
}
function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
@ -152,6 +183,22 @@ function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
);
}
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported}`;
return (
planner.proposed_primitives.includes("query_movements") &&
(combined.includes("turnover") ||
combined.includes("revenue") ||
combined.includes("payment") ||
combined.includes("payout") ||
combined.includes("value"))
);
}
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: step.primitive_id,
@ -175,7 +222,7 @@ function queryResultToProbeResult(
};
}
function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
function summarizeLifecycleRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
@ -185,6 +232,16 @@ function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
}
function summarizeValueFlowRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP value-flow rows fetched";
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function rowDateValue(row: Record<string, unknown>): string | null {
const candidates = [
row["Период"],
@ -204,6 +261,38 @@ function rowDateValue(row: Record<string, unknown>): string | null {
return null;
}
function rowAmountValue(row: Record<string, unknown>): number | null {
const candidates = [
row["Сумма"],
row["РЎСѓРјРјР°"],
row["СуммаДокумента"],
row["СуммаДокумента"],
row["Amount"],
row["amount"],
row["Total"],
row["total"]
];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate)) {
return candidate;
}
const text = toNonEmptyString(candidate);
if (!text) {
continue;
}
const normalized = text
.replace(/\s+/g, "")
.replace(/\u00a0/g, "")
.replace(",", ".")
.replace(/[^\d.-]/g, "");
const parsed = Number(normalized);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function monthDiff(firstIsoDate: string, latestIsoDate: string): number {
const first = new Date(`${firstIsoDate}T00:00:00.000Z`);
const latest = new Date(`${latestIsoDate}T00:00:00.000Z`);
@ -259,7 +348,54 @@ function deriveActivityPeriod(
};
}
function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
function formatAmountHumanRu(amount: number): string {
const formatted = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 2,
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2
})
.format(amount)
.replace(/\u00a0/g, " ");
return `${formatted} руб.`;
}
function deriveValueFlow(
result: AddressMcpQueryExecutorResult | null,
counterparty: string | null,
periodScope: string | null
): AssistantMcpDiscoveryDerivedValueFlow | null {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
let totalAmount = 0;
let rowsWithAmount = 0;
for (const row of result.rows) {
const amount = rowAmountValue(row);
if (amount !== null) {
totalAmount += amount;
rowsWithAmount += 1;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const dates = result.rows
.map((row) => rowDateValue(row))
.filter((value): value is string => Boolean(value))
.sort();
return {
counterparty,
period_scope: periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
total_amount: totalAmount,
total_amount_human_ru: formatAmountHumanRu(totalAmount),
first_movement_date: dates[0] ?? null,
latest_movement_date: dates[dates.length - 1] ?? null,
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
@ -270,17 +406,43 @@ function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty
];
}
function buildInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
function buildValueFlowConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C value-flow rows were found for counterparty ${counterparty}`
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildLifecycleInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
}
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
}
function buildUnknownFacts(): string[] {
function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueFlow | null): string[] {
if (!derived) {
return [];
}
return ["Counterparty value-flow total was calculated from confirmed 1C movement rows"];
}
function buildLifecycleUnknownFacts(): string[] {
return ["Legal registration date is not proven by this MCP discovery pilot"];
}
function buildValueFlowUnknownFacts(periodScope: string | null): string[] {
return [
periodScope
? "Full turnover outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time turnover is not proven without an explicit checked period"
];
}
function buildEmptyEvidence(
planner: AssistantMcpDiscoveryPlannerContract,
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
@ -323,6 +485,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes
};
@ -344,12 +507,16 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes
};
}
if (!isLifecyclePilotEligible(planner)) {
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@ -369,13 +536,99 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes
};
}
let queryResult: AddressMcpQueryExecutorResult | null = null;
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
if (valueFlowPilotEligible) {
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildValueFlowFilters(planner);
const selection = selectAddressRecipe("customer_revenue_and_payments", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_value_flow_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Value-flow recipe is not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Value-flow recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_movements") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_value_flow_uses_query_movements_and_derives_aggregate"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_movements_mcp_error");
} else {
pushReason(reasonCodes, "pilot_query_movements_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildValueFlowConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: buildValueFlowInferredFacts(derivedValueFlow),
unknownFacts: buildValueFlowUnknownFacts(dateScope),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "counterparty_value_flow_query_movements_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
let queryResult: AddressMcpQueryExecutorResult | null = null;
const filters = buildLifecycleFilters(planner);
const selection = selectAddressRecipe("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) {
@ -394,6 +647,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: null,
derived_activity_period: null,
derived_value_flow: null,
query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes
};
@ -421,7 +675,7 @@ export async function executeAssistantMcpDiscoveryPilot(
}
}
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
const sourceRowsSummary = queryResult ? summarizeLifecycleRows(queryResult) : null;
const derivedActivityPeriod = deriveActivityPeriod(queryResult);
if (derivedActivityPeriod) {
pushReason(reasonCodes, "pilot_derived_activity_period_from_confirmed_rows");
@ -429,9 +683,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(),
confirmedFacts: queryResult ? buildLifecycleConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildLifecycleInferredFacts(queryResult) : [],
unknownFacts: buildLifecycleUnknownFacts(),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
@ -450,6 +704,7 @@ export async function executeAssistantMcpDiscoveryPilot(
evidence,
source_rows_summary: sourceRowsSummary,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};

View File

@ -78,6 +78,7 @@ function hasInternalMechanics(value: string): boolean {
text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_") ||
@ -97,12 +98,28 @@ function localizeLine(value: string): string {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности по запрошенному контрагентскому контуру.";
}
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
if (valueFlowMatch) {
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
}
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
}
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена.";
}
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
}
return value;
}

View File

@ -73,6 +73,7 @@ function hasInternalMechanics(value: string): boolean {
text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("primitive") ||
text.includes("pilot_") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_") ||
@ -122,6 +123,20 @@ function isDiscoveryReadyChatCandidate(
);
}
function isDiscoveryReadyDeepCandidate(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const turnInput = toRecordObject(entryPoint?.turn_input);
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
return (
entryPoint?.entry_status === "bridge_executed" &&
entryPoint.discovery_attempted === true &&
turnInput?.should_run_discovery === true &&
(source === "deep_analysis" || source === "partial_coverage" || source === "normalizer_v2_0_2")
);
}
export function applyAssistantMcpDiscoveryResponsePolicy(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
): AssistantMcpDiscoveryResponsePolicyResult {
@ -133,6 +148,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@ -143,6 +159,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (!discoveryReadyChatCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
}
if (!discoveryReadyDeepCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@ -158,7 +177,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const canApply =
Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate) &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -138,17 +138,24 @@ function hasLifecycleSignal(text: string): boolean {
);
}
function hasValueFlowSignal(text: string): boolean {
return /(?:оборот|выручк|оплат|плат[её]ж|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|value[-\s]?flow|turnover|revenue|payment|payout|cash\s+flow)/iu.test(
text
);
}
function semanticNeedFor(input: {
domain: string | null;
action: string | null;
unsupported: string | null;
lifecycleSignal: boolean;
valueFlowSignal: boolean;
}): string | null {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
if (/(?:document|documents|list_documents)/iu.test(combined)) {
@ -163,12 +170,16 @@ function semanticNeedFor(input: {
function shouldRunDiscovery(input: {
unsupported: string | null;
lifecycleSignal: boolean;
valueFlowSignal: boolean;
semanticDataNeed: string | null;
explicitIntentCandidate: string | null;
}): boolean {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
@ -184,6 +195,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const valueFlowSignal = !lifecycleSignal && hasValueFlowSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
@ -193,19 +205,25 @@ export function buildAssistantMcpDiscoveryTurnInput(
domain: rawDomain,
action: rawAction,
unsupported,
lifecycleSignal
lifecycleSignal,
valueFlowSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
if (valueFlowSignal && !predecomposeEntities.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization);
}
const explicitOrganizationScope =
valueFlowSignal && !predecomposeEntities.counterparty ? null : predecomposeEntities.organization;
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal ? "turnover" : rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value_or_turnover" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal)
};
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
@ -234,6 +252,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const runDiscovery = shouldRunDiscovery({
unsupported,
lifecycleSignal,
valueFlowSignal,
semanticDataNeed,
explicitIntentCandidate
});
@ -244,11 +263,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "predecompose_contract"
: lifecycleSignal
? "raw_text"
: valueFlowSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
if (valueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}

View File

@ -110,7 +110,10 @@ describe("assistant deep turn response runtime adapter", () => {
})
);
expect(runtime.response).toEqual(responsePayload);
expect(runtime.debug).toEqual({ trace_id: "trace-1" });
expect(runtime.debug).toMatchObject({
trace_id: "trace-1",
mcp_discovery_response_applied: false
});
});
it("passes feature flags and followup flags into packaging stage", () => {
@ -160,4 +163,86 @@ describe("assistant deep turn response runtime adapter", () => {
})
);
});
it("can replace a bad deep partial answer with a guarded MCP discovery candidate", () => {
const runPackagingRuntime = vi.fn(() => ({
messageId: "msg-a1",
investigationStateSnapshot: null,
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: [],
safeAssistantReplyBase: "bad-base",
safeAssistantReply: "bad deep partial answer",
debug: { trace_id: "trace-1" },
assistantItem: {
message_id: "msg-a1",
session_id: "asst-1",
role: "assistant",
text: "bad deep partial answer",
reply_type: "partial_coverage",
created_at: "2026-04-10T00:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: {}
}));
const runFinalizeDeepTurn = vi.fn((input) => ({
response: {
ok: true,
session_id: "asst-1",
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.assistantItem,
conversation: [],
debug: input.debug
}
}));
const runtime = runAssistantDeepTurnResponseRuntime(
buildBaseInput({
composition: { reply_type: "partial_coverage" },
addressRuntimeMetaForDeep: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { should_run_discovery: true },
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: "Discovery answer",
confirmed_lines: ["Confirmed fact"],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: []
}
},
runPackagingRuntime,
runFinalizeDeepTurn
})
);
expect(runtime.response.assistant_reply).toContain("Discovery answer");
expect(runtime.debug?.mcp_discovery_response_applied).toBe(true);
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
expect.objectContaining({
assistantReply: expect.stringContaining("Discovery answer"),
replyType: "partial_coverage",
assistantItem: expect.objectContaining({
text: expect.stringContaining("Discovery answer")
})
})
);
});
});

View File

@ -80,6 +80,36 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("turns value-flow evidence into a bounded turnover answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const confirmedText = draft.confirmed_lines.join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("денежных движений");
expect(confirmedText).toContain("3 750,50 руб.");
expect(confirmedText).toContain("2020-01-15");
expect(confirmedText).toContain("2020-02-20");
expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot");
expect(draft.must_not_claim).toContain("Do not claim full all-time turnover unless the checked period and coverage prove it.");
expect(draft.limitation_lines.join("\n")).not.toContain("pilot_");
});
it("does not leak primitive names or query text into user-facing lines", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -76,6 +76,49 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
});
it("executes value-flow query_movements and derives a guarded turnover sum", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
const deps = buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["query_movements"]);
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "aggregate_by_axis", "probe_coverage"]);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.evidence.confirmed_facts[0]).toContain("value-flow rows");
expect(result.source_rows_summary).toBe("2 MCP value-flow rows fetched, 2 matched value-flow scope");
expect(result.derived_value_flow).toMatchObject({
counterparty: "SVK",
period_scope: "2020",
rows_matched: 2,
rows_with_amount: 2,
total_amount: 3750.5,
first_movement_date: "2020-01-15",
latest_movement_date: "2020-02-20",
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
});
expect(result.reason_codes).toContain("pilot_query_movements_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_value_flow_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
expect(String(call?.query ?? "")).toContain("ПоступлениеНаРасчетныйСчет");
expect(call?.limit).toBeGreaterThan(0);
});
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -44,6 +44,38 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("primitive");
});
it("localizes value-flow evidence without leaking pilot mechanics", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.",
confirmed_lines: [
"1C value-flow rows were found for counterparty SVK",
"По найденным строкам денежных движений в 1С по контрагенту SVK за период 2020 сумма составляет 3 750 руб."
],
inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"],
unknown_lines: ["Full turnover outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: ["pilot_value_flow_uses_query_movements_and_derives_aggregate"],
next_step_line: null
}
}
})
);
expect(candidate.candidate_status).toBe("ready_for_guarded_use");
expect(candidate.reply_text).toContain("В 1С найдены строки денежных движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("3 750 руб.");
expect(candidate.reply_text).toContain("Полный оборот вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("pilot_");
expect(candidate.reply_text).not.toContain("query_movements");
});
it("returns not applicable when discovery was skipped for an exact supported route", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",

View File

@ -89,6 +89,26 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
});
it("applies a guarded candidate for discovery-ready deep partial answers", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "wrong deep partial answer",
currentReplySource: "deep_analysis",
addressRuntimeMeta: {
mcpDiscoveryRuntimeEntryPoint: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true
}
})
}
});
expect(result.applied).toBe(true);
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
expect(result.reply_text).toContain("Confirmed fact");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
});
it("keeps the current reply when the candidate has no grounded text", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "route is not wired",

View File

@ -64,12 +64,17 @@ describe("assistant MCP discovery runtime entry point", () => {
entities: { counterparty: "Группа СВК" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildDeps([])
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: 2500, Counterparty: "SVK" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.bridge?.bridge_status).toBe("unsupported");
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow?.total_amount).toBe(3750);
expect(result.bridge?.hot_runtime_wired).toBe(false);
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
});

View File

@ -49,6 +49,43 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_lifecycle_signal_detected");
});
it("bootstraps value-flow discovery from raw turnover wording when no exact route owns it", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "какой денежный поток был у Группа СВК за 2020 год?",
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
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.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_value_flow_signal_detected");
});
it("treats value-flow organization-shaped target as entity candidate when counterparty is absent", () => {
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.turn_meaning_ref?.explicit_entity_candidates).toEqual(["Группа СВК"]);
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
});
it("does not activate discovery for supported exact current-turn intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {