ARCH: перезапустить план на MCP bounded autonomy и добавить metadata pilot

This commit is contained in:
dctouch 2026-04-21 22:04:23 +03:00
parent bda7ca9cc1
commit 561b4ea45c
15 changed files with 1214 additions and 57 deletions

View File

@ -1345,6 +1345,22 @@ Module progress:
- Big Block 5 MCP Semantic Data Agent: `100%`.
## Reset Hand-Off - 2026-04-21
The progress updates above closed the first guarded MCP discovery pilot wave.
They do **not** mean the strategic autonomy target is complete.
From 2026-04-21 onward the mainline continues in:
- [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](/x:/1C/NDC_1C/docs/ARCH/11%20-%20architecture_turnaround/15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md:1)
That reset freezes the continuity/authority stabilization as sufficient and returns the project to the primary trajectory:
- metadata-first self-navigation;
- entity/schema grounding;
- planner-selected MCP primitive chains instead of route-per-question hardcoding.
## Execution Rule
Do not implement this plan as:

View File

@ -0,0 +1,201 @@
# 15 - MCP Bounded Autonomy Reset Plan (2026-04-21)
## Purpose
This note resets the execution focus after the stabilization wave inside turnaround `11`.
It does not cancel the continuity and authority work already done.
It clarifies that the main project trajectory is not:
- endless polishing of deterministic route arbitration;
- route-per-question hardcoding;
- a fake "agentic" mode that is actually another brittle prompt wrapper.
The real trajectory is:
- bounded assistant autonomy over reviewed MCP primitives;
- proof-first discovery of 1C evidence;
- gradual reduction of route hardcoding at the question level.
## Why The Reset Is Necessary
The previous wave repaired real defects:
- stale carryover stopped beating the current question on critical contours;
- guarded MCP discovery answers stopped being overwritten by stale exact/lifecycle paths;
- broad business evaluation no longer breaks the return into the data contour.
That work stays valid.
But it was support work around the edge of the main target.
The strategic target was always bigger:
- the assistant should not need one deterministic route per business wording;
- the assistant should be able to orient itself inside 1C through a reviewed set of MCP primitives;
- the planner should decide which safe primitive chain to use for the current data need;
- the evidence gate should decide what may be stated to the user.
So the reset is not "we were wrong".
It is:
- stabilization is now frozen as sufficient;
- the mainline returns to `MCP-first bounded autonomy`.
## What Is Frozen
The following is now baseline, not the mainline:
- current-turn meaning authority;
- continuity subordinated to current explicit meaning;
- guarded discovery response replacement;
- broad-evaluation bridge that does not destroy the next data follow-up.
These seams may still receive bug fixes if they block MCP-first execution.
They are not the main feature track anymore.
## North Star
The target is not an unrestricted free agent.
The target is a bounded planner over a reviewed primitive catalog:
1. recognize the business data need;
2. pick allowed MCP primitives;
3. execute bounded probes against 1C;
4. aggregate evidence;
5. answer only within the evidence gate.
In short:
- move determinism from `route per user wording`
- to `catalog of safe primitives + proof workflow`.
## Big Block A. Metadata-First Self-Navigation
### Goal
Teach the assistant to inspect the 1C schema surface before guessing a route.
This is the first real step from pilot hardcoding toward self-navigation.
### Scope
- live execution of `inspect_1c_metadata`;
- metadata-aware planner path that cannot collapse into `query_documents`;
- machine-readable metadata evidence:
- available object sets;
- matching objects;
- available fields/sections when metadata returns them;
- known limitations;
- human-safe answer draft for metadata discovery.
### Why This Block Comes First
Without metadata-first inspection, every later autonomy step is blind:
- entity resolution is guesswork;
- register/document choice is guesswork;
- long-tail discovery turns back into hidden route hardcoding.
### Acceptance
- raw metadata wording can bootstrap discovery input;
- planner keeps `inspect_1c_metadata` as the chosen primitive;
- pilot executes live metadata inspection through MCP;
- user-facing answer stays free of primitive/query/runtime garbage.
## Big Block B. Entity And Schema Grounding
### Goal
Move from "I found some metadata" to "I can ground the user ask onto the right 1C surface".
### Scope
- search and resolve candidate entities through MCP instead of local tails only;
- bind metadata findings to probable document/register families;
- preserve ambiguity honestly when multiple surfaces compete;
- keep machine-readable grounding evidence for downstream probes.
### Acceptance
- assistant can say which schema surface it selected and why;
- ambiguity is surfaced as a bounded clarification, not silent route drift;
- chosen surface becomes reusable context for the next primitive.
## Big Block C. Planner-Selected Primitive Chains
### Goal
Replace one-off pilot scopes with planner-selected safe chains.
### Scope
- chain primitives such as:
- `inspect_1c_metadata`
- `search_business_entity`
- `resolve_entity_reference`
- `query_documents`
- `query_movements`
- `aggregate_by_axis`
- `probe_coverage`
- `explain_evidence_basis`
- keep exact deterministic routes as fast-paths;
- use discovery as the general path for understood long-tail questions.
### Acceptance
- new long-tail questions become answerable without adding a dedicated route for each wording;
- the planner output explains the chosen primitive chain;
- the answer gate still blocks overclaiming.
## Stage 1 Started Now
The first block is no longer just planned.
It has started in code in this pass.
Implemented in this stage:
- raw metadata wording now bootstraps discovery input;
- metadata planning stays on `inspect_1c_metadata` and no longer falls into `query_documents`;
- the pilot executor now has a live metadata inspection slice;
- the answer adapter can produce a user-safe metadata surface answer.
This stage is intentionally narrow.
It does **not** yet mean:
- unrestricted Qwen3 navigation across arbitrary 1C contours;
- automatic multi-step schema-to-entity-to-query chaining;
- hot-runtime replacement of broad assistant behavior everywhere.
It means the architecture now has the first real self-navigation primitive in production code.
## Execution Rule
From this point the project should prefer:
- adding or strengthening reviewed MCP primitives;
- planner-selected evidence workflows;
- machine-readable grounding and proof contracts.
And should avoid:
- growing another layer of hidden route hardcoding for each new wording;
- long stabilization detours unless they protect an MCP-first invariant;
- fake autonomy that bypasses the evidence gate.
## Bottom Line
Turnaround `11` is no longer only about making the assistant feel less glitchy.
The next move is larger:
- make the assistant able to look into 1C through bounded MCP discovery,
- choose its path through reviewed primitives,
- and answer from proved evidence instead of memorized route scripts.

View File

@ -64,9 +64,15 @@ function isValueFlowPilot(pilot) {
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" ||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1");
}
function isMetadataPilot(pilot) {
return pilot.pilot_scope === "metadata_inspection_v1";
}
function headlineFor(mode, pilot) {
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
}
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
}
@ -124,6 +130,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 (isMetadataPilot(pilot)) {
claims.push("Do not present metadata surface as confirmed business data rows.");
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
@ -172,6 +182,23 @@ function derivedActivityInferenceLine(pilot) {
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
].join(" ");
}
function derivedMetadataConfirmedLine(pilot) {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
const entitySets = surface.available_entity_sets.length > 0
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.`
: "";
const objects = surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: "";
const fields = surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${fields}`.replace(/\s+/g, " ").trim();
}
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
@ -262,6 +289,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
@ -271,6 +299,8 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
}
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
: pilot.evidence.confirmed_facts;
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,

View File

@ -8,7 +8,8 @@ const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
const DEFAULT_DEPS = {
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
executeAddressMcpMetadata: addressMcpClient_1.executeAddressMcpMetadata
};
function toNonEmptyString(value) {
if (value === null || value === undefined) {
@ -119,6 +120,55 @@ function isValueFlowPilotEligible(planner) {
combined.includes("payout") ||
combined.includes("value")));
}
function isMetadataPilotEligible(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 semanticNeed = String(planner.semantic_data_need ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`;
return (planner.proposed_primitives.includes("inspect_1c_metadata") &&
(combined.includes("metadata") ||
combined.includes("schema") ||
combined.includes("catalog") ||
combined.includes("inspect_documents") ||
combined.includes("inspect_registers") ||
combined.includes("inspect_fields")));
}
function metadataScopeForPlanner(planner) {
const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) {
return entityCandidate;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}`
.toLowerCase()
.trim();
if (combined.includes("vat")) {
return "НДС";
}
if (combined.includes("inventory")) {
return "склад";
}
if (combined.includes("counterparty")) {
return "контрагент";
}
return null;
}
function metadataTypesForPlanner(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
if (action === "inspect_registers") {
return ["РегистрНакопления", "РегистрСведений"];
}
if (action === "inspect_documents") {
return ["Документ"];
}
if (action === "inspect_catalog") {
return ["Справочник"];
}
return ["Документ", "РегистрНакопления", "РегистрСведений", "Справочник"];
}
function valueFlowPilotProfile(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@ -168,6 +218,15 @@ function queryResultToProbeResult(primitiveId, result) {
limitation: result.error
};
}
function metadataResultToProbeResult(primitiveId, result) {
return {
primitive_id: primitiveId,
status: result.error ? "error" : "ok",
rows_received: result.fetched_rows,
rows_matched: result.error ? 0 : result.rows.length,
limitation: result.error
};
}
function toCoverageAwareQueryResult(result, options = {}) {
if (!result) {
return null;
@ -332,6 +391,147 @@ function summarizeValueFlowRows(result) {
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function summarizeMetadataRows(result) {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP metadata rows fetched";
}
return `${result.fetched_rows} MCP metadata rows fetched`;
}
function metadataRowText(row, keys) {
for (const key of keys) {
const text = toNonEmptyString(row[key]);
if (text) {
return text;
}
}
return null;
}
function metadataObjectName(row) {
return metadataRowText(row, [
"ПолноеИмя",
"full_name",
"FullName",
"Имя",
"name",
"Name",
"presentation",
"Представление",
"synonym",
"Synonym"
]);
}
function metadataEntitySet(row) {
return metadataRowText(row, [
"ТипМетаданных",
"type",
"Type",
"meta_type",
"MetaType",
"ВидМетаданных",
"kind"
]);
}
function metadataChildNames(value) {
if (!Array.isArray(value)) {
return [];
}
const result = [];
for (const item of value) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
continue;
}
const record = item;
const fieldName = metadataRowText(record, ["Имя", "name", "Name", "full_name", "FullName"]);
if (fieldName) {
pushUnique(result, fieldName);
}
}
return result;
}
function metadataAvailableFields(rows) {
const result = [];
for (const row of rows) {
for (const field of metadataChildNames(row["Реквизиты"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["attributes"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["Attributes"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["Измерения"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["dimensions"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["Ресурсы"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["resources"])) {
pushUnique(result, field);
}
}
return result;
}
function deriveMetadataSurface(result, metadataScope, requestedMetaTypes) {
if (!result || result.error || result.rows.length <= 0) {
return null;
}
const matchedObjects = [];
const availableEntitySets = [];
for (const row of result.rows) {
const objectName = metadataObjectName(row);
if (objectName) {
pushUnique(matchedObjects, objectName);
}
const entitySet = metadataEntitySet(row);
if (entitySet) {
pushUnique(availableEntitySets, entitySet);
}
}
return {
metadata_scope: metadataScope,
requested_meta_types: requestedMetaTypes,
matched_rows: result.rows.length,
available_entity_sets: availableEntitySets,
matched_objects: matchedObjects,
available_fields: metadataAvailableFields(result.rows),
known_limitations: [],
inference_basis: "confirmed_1c_metadata_surface_rows"
};
}
function buildMetadataConfirmedFacts(surface) {
if (!surface) {
return [];
}
const facts = [];
const scopeSuffix = surface.metadata_scope ? ` for ${surface.metadata_scope}` : "";
facts.push(`Confirmed 1C metadata surface${scopeSuffix}: ${surface.matched_rows} rows and ${surface.matched_objects.length} matching objects`);
if (surface.available_entity_sets.length > 0) {
facts.push(`Available metadata object sets: ${surface.available_entity_sets.join(", ")}`);
}
if (surface.available_fields.length > 0) {
facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`);
}
return facts;
}
function buildMetadataUnknownFacts(surface, metadataScope) {
if (surface) {
if (surface.available_fields.length > 0) {
return [];
}
return ["Detailed metadata fields were not returned by this MCP metadata probe"];
}
if (metadataScope) {
return [`No matching 1C metadata objects were confirmed for scope "${metadataScope}"`];
}
return ["No matching 1C metadata objects were confirmed by this MCP metadata probe"];
}
function rowDateValue(row) {
const candidates = [
row["Период"],
@ -756,13 +956,27 @@ function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
recommendedNextProbe: dryRun.user_facing_fallback
});
}
function pilotScopeForPlanner(planner) {
if (isMetadataPilotEligible(planner)) {
return "metadata_inspection_v1";
}
if (isValueFlowPilotEligible(planner)) {
return valueFlowPilotProfile(planner).scope;
}
return "counterparty_lifecycle_query_documents_v1";
}
async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const runtimeDeps = {
...DEFAULT_DEPS,
...deps
};
const dryRun = (0, assistantMcpDiscoveryRuntimeAdapter_1.buildAssistantMcpDiscoveryRuntimeDryRun)(planner);
const reasonCodes = [...dryRun.reason_codes];
const executedPrimitives = [];
const skippedPrimitives = [];
const probeResults = [];
const queryLimitations = [];
const pilotScope = pilotScopeForPlanner(planner);
if (dryRun.adapter_status === "blocked") {
pushReason(reasonCodes, "pilot_blocked_before_mcp_execution");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot was blocked before execution");
@ -770,7 +984,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "blocked",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
pilot_scope: pilotScope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
@ -778,6 +992,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -792,7 +1007,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
pilot_scope: pilotScope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
@ -800,6 +1015,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -807,9 +1023,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
reason_codes: reasonCodes
};
}
const metadataPilotEligible = isMetadataPilotEligible(planner);
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
if (!metadataPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@ -820,7 +1037,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
pilot_scope: pilotScope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
@ -828,6 +1045,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -838,6 +1056,65 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const aggregationAxis = aggregationAxisForPlanner(planner);
if (metadataPilotEligible) {
let metadataResult = null;
const metadataScope = metadataScopeForPlanner(planner);
const requestedMetaTypes = metadataTypesForPlanner(planner);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "inspect_1c_metadata") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_metadata_uses_only_inspect_1c_metadata"));
continue;
}
metadataResult = await runtimeDeps.executeAddressMcpMetadata({
meta_type: requestedMetaTypes,
name_mask: metadataScope ?? undefined,
limit: planner.discovery_plan.execution_budget.max_rows_per_probe
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(metadataResultToProbeResult(step.primitive_id, metadataResult));
if (metadataResult.error) {
pushUnique(queryLimitations, metadataResult.error);
pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_executed");
}
}
const sourceRowsSummary = metadataResult ? summarizeMetadataRows(metadataResult) : null;
const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes);
if (derivedMetadataSurface) {
pushReason(reasonCodes, "pilot_derived_metadata_surface_from_confirmed_rows");
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildMetadataConfirmedFacts(derivedMetadataSurface),
unknownFacts: buildMetadataUnknownFacts(derivedMetadataSurface, metadataScope),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "inspect_1c_metadata"
});
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "metadata_inspection_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_metadata_surface: derivedMetadataSurface,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
if (valueFlowPilotEligible) {
let queryResult = null;
const filters = buildValueFlowFilters(planner);
@ -862,6 +1139,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -883,7 +1161,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
dateScope,
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
deps
deps: runtimeDeps
});
const outgoingExecution = await executeCoverageAwareValueFlowQuery({
primitiveId: step.primitive_id,
@ -892,7 +1170,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
dateScope,
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
deps
deps: runtimeDeps
});
incomingResult = incomingExecution.result;
outgoingResult = outgoingExecution.result;
@ -953,6 +1231,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: derivedBidirectionalValueFlow,
@ -977,6 +1256,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1000,7 +1280,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
dateScope,
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
deps
deps: runtimeDeps
});
queryResult = execution.result;
pushUnique(executedPrimitives, step.primitive_id);
@ -1048,6 +1328,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
@ -1073,6 +1354,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1087,7 +1369,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
@ -1129,6 +1411,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
derived_bidirectional_value_flow: null,

View File

@ -98,15 +98,6 @@ function recipeFor(input) {
: "planner_selected_value_flow_recipe"
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
}
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
@ -127,6 +118,15 @@ function recipeFor(input) {
reason: "planner_selected_metadata_recipe"
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
return {

View File

@ -220,6 +220,28 @@ function hasBidirectionalValueFlowSignal(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);
}
function hasMetadataSignal(text) {
if (/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(text)) {
return true;
}
return (/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438|\u043f\u043e\u043b(?:\u0435|\u044f)|registers?|documents?|catalogs?|fields?)/iu.test(text) &&
/(?:\u0435\u0441\u0442\u044c|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|available|exist)/iu.test(text));
}
function metadataActionFromRawText(text) {
if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) {
return "inspect_fields";
}
if (/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440|register)/iu.test(text)) {
return "inspect_registers";
}
if (/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|document)/iu.test(text)) {
return "inspect_documents";
}
if (/(?:\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a|directory|catalog)/iu.test(text)) {
return "inspect_catalog";
}
return "inspect_catalog";
}
function hasExplicitDateScopeLiteral(text) {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
}
@ -240,6 +262,9 @@ function collectDateScopeFromRawText(text) {
}
function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) {
return "1C metadata evidence";
}
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
@ -249,15 +274,15 @@ function semanticNeedFor(input) {
if (/(?:document|documents|list_documents)/iu.test(combined)) {
return "document evidence";
}
if (/(?:metadata|schema|catalog)/iu.test(combined)) {
return "1C metadata evidence";
}
return null;
}
function shouldRunDiscovery(input) {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (input.metadataSignal) {
return true;
}
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
@ -280,6 +305,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
@ -311,7 +337,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal
valueFlowSignal,
metadataSignal: rawMetadataSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
@ -329,6 +356,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "counterparty_lifecycle"
: valueFlowSignal
? "counterparty_value"
: rawMetadataSignal
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal
? "activity_duration"
@ -338,6 +367,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: payoutSignal
? "payout"
: rawAction ?? seededAction ?? "turnover"
: rawMetadataSignal
? metadataActionFromRawText(rawText)
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates,
@ -352,6 +383,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: payoutSignal
? "counterparty_payouts_or_outflow"
: seededUnsupported ?? "counterparty_value_or_turnover"
: rawMetadataSignal
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
@ -359,6 +392,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
rawMetadataSignal ||
followupDiscoverySeedApplicable)
};
const cleanTurnMeaning = {};
@ -390,6 +424,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal,
semanticDataNeed,
explicitIntentCandidate,
followupDiscoverySeedApplicable
@ -404,6 +439,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: lifecycleSignal
? "raw_text"
: valueFlowSignal
? "raw_text"
: rawMetadataSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
@ -412,6 +449,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (valueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
}
if (rawMetadataSignal) {
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}

View File

@ -97,10 +97,17 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b
);
}
function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "metadata_inspection_v1";
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
}
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
}
@ -160,6 +167,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 (isMetadataPilot(pilot)) {
claims.push("Do not present metadata surface as confirmed business data rows.");
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
@ -213,6 +224,27 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
].join(" ");
}
function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
const entitySets =
surface.available_entity_sets.length > 0
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.`
: "";
const objects =
surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: "";
const fields =
surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${fields}`.replace(/\s+/g, " ").trim();
}
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_value_flow;
if (!flow) {
@ -318,6 +350,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines =
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
@ -328,6 +361,8 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
}
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
: pilot.evidence.confirmed_facts;
return {

View File

@ -1,4 +1,5 @@
import {
executeAddressMcpMetadata,
executeAddressMcpQuery,
type AddressMcpMetadataRowsResult
} from "./addressMcpClient";
@ -26,7 +27,13 @@ export type AssistantMcpDiscoveryPilotStatus =
| "unsupported";
export interface AssistantMcpDiscoveryPilotExecutorDeps {
executeAddressMcpQuery?: typeof executeAddressMcpQuery;
executeAddressMcpMetadata?: typeof executeAddressMcpMetadata;
}
interface ResolvedAssistantMcpDiscoveryPilotExecutorDeps {
executeAddressMcpQuery: typeof executeAddressMcpQuery;
executeAddressMcpMetadata: typeof executeAddressMcpMetadata;
}
export interface AssistantMcpDiscoveryDerivedActivityPeriod {
@ -109,6 +116,17 @@ export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow {
inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows";
}
export interface AssistantMcpDiscoveryDerivedMetadataSurface {
metadata_scope: string | null;
requested_meta_types: string[];
matched_rows: number;
available_entity_sets: string[];
matched_objects: string[];
available_fields: string[];
known_limitations: string[];
inference_basis: "confirmed_1c_metadata_surface_rows";
}
interface AssistantMcpDiscoveryCoverageAwareQueryResult extends AddressMcpQueryExecutorResult {
coverage_limited_by_probe_limit: boolean;
coverage_recovered_by_period_chunking: boolean;
@ -124,6 +142,7 @@ interface AssistantMcpDiscoveryCoverageAwareQueryExecution {
}
export type AssistantMcpDiscoveryPilotScope =
| "metadata_inspection_v1"
| "counterparty_lifecycle_query_documents_v1"
| "counterparty_value_flow_query_movements_v1"
| "counterparty_supplier_payout_query_movements_v1"
@ -141,6 +160,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
probe_results: AssistantMcpDiscoveryProbeResult[];
evidence: AssistantMcpDiscoveryEvidenceContract;
source_rows_summary: string | null;
derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
@ -150,8 +170,9 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
type AddressMcpQueryExecutorResult = Awaited<ReturnType<typeof executeAddressMcpQuery>>;
const DEFAULT_DEPS: AssistantMcpDiscoveryPilotExecutorDeps = {
executeAddressMcpQuery
const DEFAULT_DEPS: ResolvedAssistantMcpDiscoveryPilotExecutorDeps = {
executeAddressMcpQuery,
executeAddressMcpMetadata
};
function toNonEmptyString(value: unknown): string | null {
@ -280,6 +301,60 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
);
}
function isMetadataPilotEligible(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 semanticNeed = String(planner.semantic_data_need ?? "").toLowerCase();
const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`;
return (
planner.proposed_primitives.includes("inspect_1c_metadata") &&
(combined.includes("metadata") ||
combined.includes("schema") ||
combined.includes("catalog") ||
combined.includes("inspect_documents") ||
combined.includes("inspect_registers") ||
combined.includes("inspect_fields"))
);
}
function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) {
return entityCandidate;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}`
.toLowerCase()
.trim();
if (combined.includes("vat")) {
return "НДС";
}
if (combined.includes("inventory")) {
return "склад";
}
if (combined.includes("counterparty")) {
return "контрагент";
}
return null;
}
function metadataTypesForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] {
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
if (action === "inspect_registers") {
return ["РегистрНакопления", "РегистрСведений"];
}
if (action === "inspect_documents") {
return ["Документ"];
}
if (action === "inspect_catalog") {
return ["Справочник"];
}
return ["Документ", "РегистрНакопления", "РегистрСведений", "Справочник"];
}
interface ValueFlowPilotProfile {
scope: Extract<
AssistantMcpDiscoveryPilotScope,
@ -350,6 +425,19 @@ function queryResultToProbeResult(
};
}
function metadataResultToProbeResult(
primitiveId: string,
result: AddressMcpMetadataRowsResult
): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: primitiveId,
status: result.error ? "error" : "ok",
rows_received: result.fetched_rows,
rows_matched: result.error ? 0 : result.rows.length,
limitation: result.error
};
}
function toCoverageAwareQueryResult(
result: AddressMcpQueryExecutorResult | null,
options: {
@ -428,7 +516,7 @@ async function executeCoverageAwareValueFlowQuery(input: {
dateScope: string | null;
maxProbeCount: number;
maxRowsPerProbe: number;
deps: AssistantMcpDiscoveryPilotExecutorDeps;
deps: ResolvedAssistantMcpDiscoveryPilotExecutorDeps;
}): Promise<AssistantMcpDiscoveryCoverageAwareQueryExecution> {
const queryLimitations: string[] = [];
const probeResults: AssistantMcpDiscoveryProbeResult[] = [];
@ -560,6 +648,167 @@ function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryR
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function summarizeMetadataRows(result: AddressMcpMetadataRowsResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP metadata rows fetched";
}
return `${result.fetched_rows} MCP metadata rows fetched`;
}
function metadataRowText(row: Record<string, unknown>, keys: string[]): string | null {
for (const key of keys) {
const text = toNonEmptyString(row[key]);
if (text) {
return text;
}
}
return null;
}
function metadataObjectName(row: Record<string, unknown>): string | null {
return metadataRowText(row, [
"ПолноеИмя",
"full_name",
"FullName",
"Имя",
"name",
"Name",
"presentation",
"Представление",
"synonym",
"Synonym"
]);
}
function metadataEntitySet(row: Record<string, unknown>): string | null {
return metadataRowText(row, [
"ТипМетаданных",
"type",
"Type",
"meta_type",
"MetaType",
"ВидМетаданных",
"kind"
]);
}
function metadataChildNames(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
const result: string[] = [];
for (const item of value) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
continue;
}
const record = item as Record<string, unknown>;
const fieldName = metadataRowText(record, ["Имя", "name", "Name", "full_name", "FullName"]);
if (fieldName) {
pushUnique(result, fieldName);
}
}
return result;
}
function metadataAvailableFields(rows: Array<Record<string, unknown>>): string[] {
const result: string[] = [];
for (const row of rows) {
for (const field of metadataChildNames(row["Реквизиты"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["attributes"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["Attributes"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["Измерения"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["dimensions"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["Ресурсы"])) {
pushUnique(result, field);
}
for (const field of metadataChildNames(row["resources"])) {
pushUnique(result, field);
}
}
return result;
}
function deriveMetadataSurface(
result: AddressMcpMetadataRowsResult | null,
metadataScope: string | null,
requestedMetaTypes: string[]
): AssistantMcpDiscoveryDerivedMetadataSurface | null {
if (!result || result.error || result.rows.length <= 0) {
return null;
}
const matchedObjects: string[] = [];
const availableEntitySets: string[] = [];
for (const row of result.rows) {
const objectName = metadataObjectName(row);
if (objectName) {
pushUnique(matchedObjects, objectName);
}
const entitySet = metadataEntitySet(row);
if (entitySet) {
pushUnique(availableEntitySets, entitySet);
}
}
return {
metadata_scope: metadataScope,
requested_meta_types: requestedMetaTypes,
matched_rows: result.rows.length,
available_entity_sets: availableEntitySets,
matched_objects: matchedObjects,
available_fields: metadataAvailableFields(result.rows),
known_limitations: [],
inference_basis: "confirmed_1c_metadata_surface_rows"
};
}
function buildMetadataConfirmedFacts(
surface: AssistantMcpDiscoveryDerivedMetadataSurface | null
): string[] {
if (!surface) {
return [];
}
const facts: string[] = [];
const scopeSuffix = surface.metadata_scope ? ` for ${surface.metadata_scope}` : "";
facts.push(
`Confirmed 1C metadata surface${scopeSuffix}: ${surface.matched_rows} rows and ${surface.matched_objects.length} matching objects`
);
if (surface.available_entity_sets.length > 0) {
facts.push(`Available metadata object sets: ${surface.available_entity_sets.join(", ")}`);
}
if (surface.available_fields.length > 0) {
facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`);
}
return facts;
}
function buildMetadataUnknownFacts(
surface: AssistantMcpDiscoveryDerivedMetadataSurface | null,
metadataScope: string | null
): string[] {
if (surface) {
if (surface.available_fields.length > 0) {
return [];
}
return ["Detailed metadata fields were not returned by this MCP metadata probe"];
}
if (metadataScope) {
return [`No matching 1C metadata objects were confirmed for scope "${metadataScope}"`];
}
return ["No matching 1C metadata objects were confirmed by this MCP metadata probe"];
}
function rowDateValue(row: Record<string, unknown>): string | null {
const candidates = [
row["Период"],
@ -1072,16 +1321,31 @@ function buildEmptyEvidence(
});
}
function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryPilotScope {
if (isMetadataPilotEligible(planner)) {
return "metadata_inspection_v1";
}
if (isValueFlowPilotEligible(planner)) {
return valueFlowPilotProfile(planner).scope;
}
return "counterparty_lifecycle_query_documents_v1";
}
export async function executeAssistantMcpDiscoveryPilot(
planner: AssistantMcpDiscoveryPlannerContract,
deps: AssistantMcpDiscoveryPilotExecutorDeps = DEFAULT_DEPS
): Promise<AssistantMcpDiscoveryPilotExecutionContract> {
const runtimeDeps: ResolvedAssistantMcpDiscoveryPilotExecutorDeps = {
...DEFAULT_DEPS,
...deps
};
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
const reasonCodes = [...dryRun.reason_codes];
const executedPrimitives: string[] = [];
const skippedPrimitives: string[] = [];
const probeResults: AssistantMcpDiscoveryProbeResult[] = [];
const queryLimitations: string[] = [];
const pilotScope = pilotScopeForPlanner(planner);
if (dryRun.adapter_status === "blocked") {
pushReason(reasonCodes, "pilot_blocked_before_mcp_execution");
@ -1090,7 +1354,7 @@ export async function executeAssistantMcpDiscoveryPilot(
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "blocked",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
pilot_scope: pilotScope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
@ -1098,6 +1362,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1113,7 +1378,7 @@ export async function executeAssistantMcpDiscoveryPilot(
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
pilot_scope: pilotScope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
@ -1121,6 +1386,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1129,10 +1395,11 @@ export async function executeAssistantMcpDiscoveryPilot(
};
}
const metadataPilotEligible = isMetadataPilotEligible(planner);
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
if (!lifecyclePilotEligible && !valueFlowPilotEligible) {
if (!metadataPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@ -1143,7 +1410,7 @@ export async function executeAssistantMcpDiscoveryPilot(
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
pilot_scope: pilotScope,
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
@ -1151,6 +1418,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1163,6 +1431,68 @@ export async function executeAssistantMcpDiscoveryPilot(
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const aggregationAxis = aggregationAxisForPlanner(planner);
if (metadataPilotEligible) {
let metadataResult: AddressMcpMetadataRowsResult | null = null;
const metadataScope = metadataScopeForPlanner(planner);
const requestedMetaTypes = metadataTypesForPlanner(planner);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "inspect_1c_metadata") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_metadata_uses_only_inspect_1c_metadata"));
continue;
}
metadataResult = await runtimeDeps.executeAddressMcpMetadata({
meta_type: requestedMetaTypes,
name_mask: metadataScope ?? undefined,
limit: planner.discovery_plan.execution_budget.max_rows_per_probe
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(metadataResultToProbeResult(step.primitive_id, metadataResult));
if (metadataResult.error) {
pushUnique(queryLimitations, metadataResult.error);
pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_error");
} else {
pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_executed");
}
}
const sourceRowsSummary = metadataResult ? summarizeMetadataRows(metadataResult) : null;
const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes);
if (derivedMetadataSurface) {
pushReason(reasonCodes, "pilot_derived_metadata_surface_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildMetadataConfirmedFacts(derivedMetadataSurface),
unknownFacts: buildMetadataUnknownFacts(derivedMetadataSurface, metadataScope),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "inspect_1c_metadata"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "metadata_inspection_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_metadata_surface: derivedMetadataSurface,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
if (valueFlowPilotEligible) {
let queryResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
const filters = buildValueFlowFilters(planner);
@ -1187,6 +1517,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1213,7 +1544,7 @@ export async function executeAssistantMcpDiscoveryPilot(
dateScope,
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
deps
deps: runtimeDeps
});
const outgoingExecution = await executeCoverageAwareValueFlowQuery({
primitiveId: step.primitive_id,
@ -1222,7 +1553,7 @@ export async function executeAssistantMcpDiscoveryPilot(
dateScope,
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
deps
deps: runtimeDeps
});
incomingResult = incomingExecution.result;
outgoingResult = outgoingExecution.result;
@ -1285,6 +1616,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: derivedBidirectionalValueFlow,
@ -1310,6 +1642,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1337,7 +1670,7 @@ export async function executeAssistantMcpDiscoveryPilot(
dateScope,
maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count,
maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe,
deps
deps: runtimeDeps
});
queryResult = execution.result;
pushUnique(executedPrimitives, step.primitive_id);
@ -1392,6 +1725,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
@ -1418,6 +1752,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
@ -1433,7 +1768,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
@ -1476,6 +1811,7 @@ export async function executeAssistantMcpDiscoveryPilot(
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_activity_period: derivedActivityPeriod,
derived_value_flow: null,
derived_bidirectional_value_flow: null,

View File

@ -148,16 +148,6 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
}
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
@ -180,6 +170,16 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
return {

View File

@ -306,6 +306,38 @@ function hasMonthlyAggregationSignal(text: string): boolean {
);
}
function hasMetadataSignal(text: string): boolean {
if (
/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(
text
)
) {
return true;
}
return (
/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438|\u043f\u043e\u043b(?:\u0435|\u044f)|registers?|documents?|catalogs?|fields?)/iu.test(
text
) &&
/(?:\u0435\u0441\u0442\u044c|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|available|exist)/iu.test(text)
);
}
function metadataActionFromRawText(text: string): string {
if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) {
return "inspect_fields";
}
if (/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440|register)/iu.test(text)) {
return "inspect_registers";
}
if (/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|document)/iu.test(text)) {
return "inspect_documents";
}
if (/(?:\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a|directory|catalog)/iu.test(text)) {
return "inspect_catalog";
}
return "inspect_catalog";
}
function hasExplicitDateScopeLiteral(text: string): boolean {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
}
@ -332,8 +364,12 @@ function semanticNeedFor(input: {
unsupported: string | null;
lifecycleSignal: boolean;
valueFlowSignal: boolean;
metadataSignal: boolean;
}): string | null {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) {
return "1C metadata evidence";
}
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
@ -343,9 +379,6 @@ function semanticNeedFor(input: {
if (/(?:document|documents|list_documents)/iu.test(combined)) {
return "document evidence";
}
if (/(?:metadata|schema|catalog)/iu.test(combined)) {
return "1C metadata evidence";
}
return null;
}
@ -353,6 +386,7 @@ function shouldRunDiscovery(input: {
unsupported: string | null;
lifecycleSignal: boolean;
valueFlowSignal: boolean;
metadataSignal: boolean;
semanticDataNeed: string | null;
explicitIntentCandidate: string | null;
followupDiscoverySeedApplicable: boolean;
@ -360,6 +394,9 @@ function shouldRunDiscovery(input: {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (input.metadataSignal) {
return true;
}
if (input.valueFlowSignal && !input.explicitIntentCandidate) {
return true;
}
@ -386,6 +423,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal =
!rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
@ -424,7 +462,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal
valueFlowSignal,
metadataSignal: rawMetadataSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
@ -445,6 +484,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "counterparty_lifecycle"
: valueFlowSignal
? "counterparty_value"
: rawMetadataSignal
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal
? "activity_duration"
@ -454,6 +495,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
: payoutSignal
? "payout"
: rawAction ?? seededAction ?? "turnover"
: rawMetadataSignal
? metadataActionFromRawText(rawText)
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates,
@ -469,6 +512,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
: payoutSignal
? "counterparty_payouts_or_outflow"
: seededUnsupported ?? "counterparty_value_or_turnover"
: rawMetadataSignal
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
@ -477,6 +522,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
rawMetadataSignal ||
followupDiscoverySeedApplicable
)
};
@ -511,6 +557,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal,
semanticDataNeed,
explicitIntentCandidate,
followupDiscoverySeedApplicable
@ -526,6 +573,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "raw_text"
: valueFlowSignal
? "raw_text"
: rawMetadataSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
@ -534,6 +583,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (valueFlowSignal) {
pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected");
}
if (rawMetadataSignal) {
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}

View File

@ -31,6 +31,17 @@ function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown
return { executeAddressMcpQuery };
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
fetched_rows: error ? 0 : rows.length,
raw_rows: error ? [] : rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery answer adapter", () => {
it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => {
const planner = planAssistantMcpDiscovery({
@ -96,6 +107,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 metadata surface evidence into a human-safe metadata answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
explicit_entity_candidates: ["НДС"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }, { Name: "Организация" }]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const confirmedText = draft.confirmed_lines.join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("метаданным 1С");
expect(confirmedText).toContain("Подтвержденная metadata-поверхность 1С");
expect(confirmedText).toContain("Документ.СчетФактураВыданный");
expect(confirmedText).toContain("Дата");
expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows.");
});
it("turns value-flow evidence into a bounded turnover answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -30,6 +30,17 @@ function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown
return { executeAddressMcpQuery };
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
fetched_rows: error ? 0 : rows.length,
raw_rows: error ? [] : rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery pilot executor", () => {
it("executes only the lifecycle query_documents primitive through injected MCP deps", async () => {
const planner = planAssistantMcpDiscovery({
@ -92,6 +103,53 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
});
it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }, { Name: "Организация" }]
},
{
FullName: "Документ.СчетФактураПолученный",
MetaType: "Документ",
attributes: [{ Name: "Контрагент" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("metadata_inspection_v1");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.source_rows_summary).toBe("2 MCP metadata rows fetched");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "НДС",
requested_meta_types: ["Документ"],
matched_rows: 2,
available_entity_sets: ["Документ"],
matched_objects: ["Документ.СчетФактураВыданный", "Документ.СчетФактураПолученный"],
available_fields: ["Дата", "Организация", "Контрагент"],
inference_basis: "confirmed_1c_metadata_surface_rows"
});
expect(result.reason_codes).toContain("pilot_inspect_1c_metadata_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_metadata_surface_from_confirmed_rows");
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
meta_type: ["Документ"],
name_mask: "НДС"
});
});
it("executes value-flow query_movements and derives a guarded turnover sum", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -120,6 +120,19 @@ describe("assistant MCP discovery planner", () => {
expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary");
});
it("keeps metadata document inspection on inspect_1c_metadata instead of query_documents", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.proposed_primitives).not.toContain("query_documents");
});
it("does not mark an unclassified turn as executable without turn meaning context", () => {
const result = planAssistantMcpDiscovery({});

View File

@ -13,6 +13,17 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
};
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
fetched_rows: error ? 0 : rows.length,
raw_rows: error ? [] : rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery runtime entry point", () => {
it("runs the bridge for discovery-eligible lifecycle turn context", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
@ -78,4 +89,27 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.hot_runtime_wired).toBe(false);
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
});
it("runs the bridge for raw metadata wording without an exact route owner", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "какие документы и поля есть в 1С по НДС?",
deps: buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }]
}
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.semantic_data_need).toBe("1C metadata evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_fields"
});
expect(result.bridge?.pilot.pilot_scope).toBe("metadata_inspection_v1");
expect(result.bridge?.answer_draft.headline).toContain("метаданным 1С");
});
});

View File

@ -151,6 +151,24 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_monthly_aggregation_signal_detected");
});
it("bootstraps metadata discovery from raw schema wording", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "какие регистры и поля есть в 1С по НДС?"
});
expect(result.adapter_status).toBe("ready");
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.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_fields",
unsupported_but_understood_family: "1c_metadata_surface",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_signal_detected");
});
it("seeds short monthly follow-up from prior bidirectional discovery context", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а по месяцам?",