Planner Autonomy: связать inventory templates с exact runtime

This commit is contained in:
dctouch 2026-05-01 13:31:41 +03:00
parent 3f7caae668
commit a4db26a76e
7 changed files with 975 additions and 57 deletions

View File

@ -87,9 +87,16 @@ The following consolidation step promoted the accepted inventory-stock breadth b
These templates are now first-class catalog chain descriptors and can be selected by the data-need graph/planner. They reuse reviewed generic primitives (`query_movements`, `query_documents`, `aggregate_by_axis`, `drilldown_related_objects`, `probe_coverage`, `explain_evidence_basis`) and add inventory-specific axes such as `as_of_date`, `warehouse`, `supplier`, `buyer`, `quantity`, and `evidence_basis`.
The live MCP pilot intentionally marks these new inventory templates as `inventory_route_template_v1` / unsupported for live generic pilot execution until the exact inventory runtime paths are explicitly bridged into the bounded MCP executor. This keeps the architecture honest: route fabric exists, but runtime execution is still gated by the existing exact inventory capabilities and future bridging work.
The first runtime bridge for these inventory templates now delegates through existing exact inventory recipes instead of inventing a new generic inventory executor:
The runtime answer boundary now makes that distinction explicit for inventory templates:
- `inventory_stock_snapshot` -> `inventory_on_hand_as_of_date`
- `inventory_supplier_overlap` -> `inventory_supplier_stock_overlap_as_of_date`
- `inventory_purchase_provenance` -> `inventory_purchase_provenance_for_item`
- `inventory_sale_trace` -> `inventory_sale_trace_for_item`
The bridge keeps the reviewed MCP route fabric as the planner surface, but uses `addressRecipeCatalog` exact queries and account scope `41.01` as the evidence source. Root inventory templates execute through `query_movements`; selected-item provenance/sale templates execute through `query_documents`. Missing selected-item anchors remain clarification, not a guessed item.
The runtime answer boundary still makes unsupported or unconfirmed inventory states explicit:
- unsupported inventory route templates get a user-facing "template selected, live execution not yet bridged" answer instead of a generic checked-sources fallback;
- `must_not_claim` forbids presenting inventory planning as executed stock, supplier, purchase, or sale evidence;
@ -152,13 +159,20 @@ Latest validation after the inventory runtime-boundary hardening:
- `npm.cmd run build`: passed
- graphify rebuild: `5913 nodes`, `12837 edges`, `138 communities`
Latest validation after the inventory exact-runtime bridge:
- targeted runtime-bridge/answer-adapter/pilot-executor tests: passed, `70 passed`, `1 skipped`
- full MCP-discovery suite: passed, `279 passed`, `9 skipped`
- `npm.cmd run build`: passed
- graphify rebuild: `5930 nodes`, `12884 edges`, `135 communities`
## Next Step
The next safe step is to continue from catalog-instantiated inventory templates into runtime bridging and broader reviewed scoring.
The next safe step is to validate the inventory exact-runtime bridge with live replay and then continue into broader reviewed scoring.
Recommended order:
1. bridge inventory catalog templates into the bounded MCP executor only where exact inventory runtime evidence can be reused safely;
1. rerun the inventory canary and a mixed cross-stage canary against live 1C/MCP once the proxy is available;
2. broaden catalog scoring beyond explicit document/movement lane choice into unfamiliar 1C asks;
3. grow primitive descriptors only where live replay shows a real evidence gap;
4. keep phase19, phase21, phase22, value-flow, metadata ambiguity, and inventory-stock canaries as regression gates.

View File

@ -76,8 +76,9 @@ It now documents a turnaround that is already operational in code, already mater
- lifecycle now behaves as a bounded activity-window inference chain with an explicit legal-fact boundary instead of an unqualified age answer;
- current-turn value-flow aggregate questions can override narrower supported exact routes when the user asks for totals/net/payment amounts;
- broad business evaluation remains in the deterministic living-chat bridge instead of being displaced by generic metadata discovery;
- inventory stock snapshot, supplier overlap, purchase provenance, and sale trace are now reviewed catalog chain templates, while live generic MCP pilot execution remains explicitly gated until the exact inventory runtime evidence is bridged safely;
- inventory stock snapshot, supplier overlap, purchase provenance, and sale trace are now reviewed catalog chain templates; generic free-form inventory execution remains forbidden, and evidence must pass through reviewed exact recipe bridges;
- runtime bridge and answer adapter now keep unsupported inventory route templates behind an explicit user-facing boundary instead of letting template planning look like confirmed stock/supplier/purchase/sale evidence;
- inventory catalog templates now bridge through existing exact inventory recipes (`41.01` scoped stock, supplier overlap, purchase provenance, and sale trace) inside the bounded MCP discovery pilot, while missing selected-item anchors still clarify instead of guessing;
- live map sync: [20 - planner_autonomy_consolidation_2026-05-01.md](./20%20-%20planner_autonomy_consolidation_2026-05-01.md)
Current honest status:
@ -89,8 +90,8 @@ Current honest status:
- open-world bounded-autonomy readiness: `~85%`
- Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice
- active inventory-stock breadth slice progress: `100%` for the declared scenario pack, not for arbitrary inventory questions
- Planner Autonomy Consolidation progress: `~70%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, and inventory runtime-boundary honesty validated, but live inventory-template bridging and broader unfamiliar 1C asks still pending
- graph snapshot after latest rebuild: `5913 nodes`, `12837 edges`, `138 communities`
- Planner Autonomy Consolidation progress: `~74%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, and exact inventory recipe bridging validated locally, but live replay for the new bridge and broader unfamiliar 1C asks still pending
- graph snapshot after latest rebuild: `5930 nodes`, `12884 edges`, `135 communities`
- current breakpoint:
- the validated hot paths are no longer structurally broken;
- flagship continuity collapse is no longer the primary risk;
@ -132,6 +133,7 @@ Latest live proof now includes:
- latest local Planner Autonomy slice accepted: full MCP-discovery suite passed `268/268` with `9` skipped; broad MCP/living-chat/route/meaning slice passed `305/305` with `9` skipped; build passed
- inventory template lift accepted locally: catalog/data-need/planner/turn-input slice passed `139/139` with `6` skipped; full MCP-discovery slice passed `276/276` with `9` skipped; build passed; graphify stayed at `5912 nodes`, `12833 edges`, `138 communities`
- inventory runtime-boundary hardening accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `68/68` with `1` skipped; full MCP-discovery slice passed `277/277` with `9` skipped; build passed; graphify rebuilt to `5913 nodes`, `12837 edges`, `138 communities`
- inventory exact-runtime bridge accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `70/70` with `1` skipped; full MCP-discovery slice passed `279/279` with `9` skipped; build passed; graphify rebuilt to `5930 nodes`, `12884 edges`, `135 communities`
Current architectural reading:

View File

@ -296,7 +296,13 @@ function headlineFor(mode, pilot) {
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
}
if (isInventoryTemplatePilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По exact inventory runtime в 1С найдены подтвержденные строки; ответ ограничен проверенным складским/товарным срезом.";
}
if (isInventoryTemplatePilot(pilot) && mode === "checked_sources_only") {
if (pilot.mcp_execution_performed) {
return "Exact inventory runtime был проверен, но подтвержденный складской/товарный факт в найденных строках не получен.";
}
return "Инвентарный route-template уже выбран, но live-исполнение этого generic MCP контура еще не подключено; складской/товарный факт не подтвержден.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
@ -430,6 +436,9 @@ function nextStepFor(mode, pilot) {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "checked_sources_only" && isInventoryTemplatePilot(pilot)) {
if (pilot.mcp_execution_performed) {
return "Можно уточнить дату, организацию, склад, поставщика или позицию и повторить exact inventory проверку.";
}
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
@ -486,8 +495,11 @@ function buildMustNotClaim(pilot) {
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
}
if (isInventoryTemplatePilot(pilot)) {
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
if (!pilot.mcp_execution_performed) {
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
}
claims.push("Do not expose inventory_route_template_v1 or MCP primitive names in the user answer.");
claims.push("Do not claim full inventory coverage outside the checked rows, date, organization, item, or supplier scope.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");

View File

@ -107,6 +107,26 @@ function dateScopeToFilters(dateScope) {
}
return {};
}
function asOfDateFromDateScope(dateScope) {
if (!dateScope) {
return null;
}
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (dateMatch) {
return `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
}
const monthMatch = dateScope.match(/^(\d{4})-(\d{2})$/);
if (monthMatch) {
const year = Number(monthMatch[1]);
const month = Number(monthMatch[2]);
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`;
}
}
const yearMatch = dateScope.match(/^(\d{4})$/);
return yearMatch ? `${yearMatch[1]}-12-31` : null;
}
function buildLifecycleFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
@ -133,6 +153,34 @@ function buildValueFlowFilters(planner) {
sort: "period_asc"
};
}
function buildInventoryExactFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const subject = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
const asOfDate = asOfDateFromDateScope(dateScope);
const filters = {
...dateScopeToFilters(dateScope),
...(asOfDate ? { as_of_date: asOfDate } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
if (planner.selected_chain_id === "inventory_purchase_provenance" ||
planner.selected_chain_id === "inventory_sale_trace") {
return {
...filters,
...(subject ? { item: subject } : {})
};
}
if (planner.selected_chain_id === "inventory_supplier_overlap") {
return {
...filters,
...(subject ? { counterparty: subject } : {})
};
}
return filters;
}
function organizationScopeForPlanner(planner) {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
@ -340,6 +388,12 @@ function isValueFlowPilotEligible(planner) {
combined.includes("payout") ||
combined.includes("value")));
}
function isInventoryPilotEligible(planner) {
return (planner.selected_chain_id === "inventory_stock_snapshot" ||
planner.selected_chain_id === "inventory_supplier_overlap" ||
planner.selected_chain_id === "inventory_purchase_provenance" ||
planner.selected_chain_id === "inventory_sale_trace");
}
function isMetadataPilotEligible(planner) {
if (planner.selected_chain_id === "metadata_inspection" ||
planner.selected_chain_id === "metadata_lane_clarification" ||
@ -468,6 +522,32 @@ function valueFlowPilotProfile(planner) {
direction: "incoming_customer_revenue"
};
}
function inventoryIntentForPlanner(planner) {
switch (planner.selected_chain_id) {
case "inventory_stock_snapshot":
return "inventory_on_hand_as_of_date";
case "inventory_supplier_overlap":
return "inventory_supplier_stock_overlap_as_of_date";
case "inventory_purchase_provenance":
return "inventory_purchase_provenance_for_item";
case "inventory_sale_trace":
return "inventory_sale_trace_for_item";
default:
return null;
}
}
function inventoryExecutablePrimitiveForPlanner(planner) {
switch (planner.selected_chain_id) {
case "inventory_stock_snapshot":
case "inventory_supplier_overlap":
return "query_movements";
case "inventory_purchase_provenance":
case "inventory_sale_trace":
return "query_documents";
default:
return null;
}
}
function skippedProbeResult(step, limitation) {
return {
primitive_id: step.primitive_id,
@ -677,6 +757,15 @@ function summarizeValueFlowRows(result) {
}
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function summarizeInventoryRows(result) {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP inventory exact rows fetched";
}
return `${result.fetched_rows} MCP inventory exact rows fetched, ${result.matched_rows} matched inventory scope`;
}
function summarizeMetadataRows(result) {
if (result.error) {
return null;
@ -1267,6 +1356,49 @@ function rowAmountValue(row) {
}
return null;
}
function rowNumberValue(row, keys) {
for (const key of keys) {
const candidate = row[key];
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 rowTextValue(row, keys) {
for (const key of keys) {
const text = toNonEmptyString(row[key]);
if (text) {
return text;
}
}
return null;
}
function rowInventoryItemValue(row) {
return rowTextValue(row, ["Номенклатура", "Item", "item", "Товар", "Product", "product"]);
}
function rowWarehouseValue(row) {
return rowTextValue(row, ["Склад", "Warehouse", "warehouse"]);
}
function rowDocumentValue(row) {
return rowTextValue(row, ["Регистратор", "Registrator", "registrator", "Документ", "Document", "document"]);
}
function rowQuantityValue(row) {
return rowNumberValue(row, ["Количество", "Quantity", "quantity", "Qty", "qty", "Остаток", "Balance", "balance"]);
}
function rowCounterpartyValue(row) {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
@ -1663,6 +1795,109 @@ function buildBidirectionalValueFlowConfirmedFacts(derived) {
`1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=${hasIncoming ? "found" : "not_found"}, outgoing=${hasOutgoing ? "found" : "not_found"}`
];
}
function inventoryLabelRu(intent) {
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
return "связи поставщиков с товарным остатком";
}
if (intent === "inventory_purchase_provenance_for_item") {
return "закупочной истории позиции";
}
if (intent === "inventory_sale_trace_for_item") {
return "продаж по позиции";
}
return "складского среза";
}
function inventoryScopeSuffixRu(input) {
const parts = [];
if (input.organization) {
parts.push(`по организации ${input.organization}`);
}
if (input.item) {
parts.push(`по позиции ${input.item}`);
}
if (input.intent === "inventory_supplier_stock_overlap_as_of_date" && input.counterparty) {
parts.push(`по поставщику/контрагенту ${input.counterparty}`);
}
if (input.asOfDate) {
parts.push(`на ${input.asOfDate}`);
}
else if (input.dateScope) {
parts.push(`за ${input.dateScope}`);
}
return parts.length > 0 ? ` ${parts.join(", ")}` : "";
}
function inventoryRowSample(row) {
const item = rowInventoryItemValue(row);
const quantity = rowQuantityValue(row);
const warehouse = rowWarehouseValue(row);
const counterparty = rowCounterpartyValue(row);
const document = rowDocumentValue(row);
const parts = [];
if (item) {
parts.push(item);
}
if (quantity !== null) {
parts.push(`${quantity} шт.`);
}
if (warehouse) {
parts.push(`склад ${warehouse}`);
}
if (counterparty) {
parts.push(`контрагент ${counterparty}`);
}
if (document) {
parts.push(`документ ${document}`);
}
return parts.length > 0 ? parts.join(", ") : null;
}
function buildInventoryConfirmedFacts(result, planner, intent) {
if (result.error || result.matched_rows <= 0) {
return [];
}
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const item = intent === "inventory_purchase_provenance_for_item" || intent === "inventory_sale_trace_for_item"
? firstEntityCandidate(planner)
: null;
const counterparty = intent === "inventory_supplier_stock_overlap_as_of_date" ? firstEntityCandidate(planner) : null;
const scope = inventoryScopeSuffixRu({
intent,
item,
counterparty,
organization: organizationScopeForPlanner(planner),
asOfDate: asOfDateFromDateScope(dateScope),
dateScope
});
const samples = result.rows
.slice(0, 3)
.map((row) => inventoryRowSample(row))
.filter((value) => Boolean(value));
const sampleSuffix = samples.length > 0 ? ` Примеры строк: ${samples.join("; ")}.` : "";
return [`В 1С найдены подтвержденные строки ${inventoryLabelRu(intent)}${scope}: ${result.matched_rows}.${sampleSuffix}`];
}
function buildInventoryInferredFacts(result, intent) {
if (result.error || result.fetched_rows <= 0) {
return [];
}
if (result.matched_rows <= 0) {
return [
`По ${inventoryLabelRu(intent)} удалось проверить только ограниченный срез 1С; подтвержденных строк этим поиском не найдено.`
];
}
return [
`Вывод по ${inventoryLabelRu(intent)} ограничен найденными строками 1С и указанными датой, организацией, позицией или поставщиком.`
];
}
function buildInventoryUnknownFacts(result, intent, dateScope) {
const facts = [
dateScope
? `Полный товарный контур вне проверенного среза ${dateScope} не подтвержден.`
: "Полный товарный контур без явного проверенного периода или даты не подтвержден."
];
if (!result || result.error || result.matched_rows <= 0) {
facts.unshift(`Подтвержденный факт по ${inventoryLabelRu(intent)} в проверенных строках 1С не найден.`);
}
return facts;
}
function buildLifecycleInferredFacts(result) {
if (result.error || result.fetched_rows <= 0) {
return [];
@ -1831,15 +2066,6 @@ function pilotScopeForPlanner(planner) {
return "entity_resolution_search_v1";
}
}
function isLivePilotChainSupported(chainId) {
if (chainId === "inventory_stock_snapshot" ||
chainId === "inventory_supplier_overlap" ||
chainId === "inventory_purchase_provenance" ||
chainId === "inventory_sale_trace") {
return false;
}
return true;
}
async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const runtimeDeps = {
...DEFAULT_DEPS,
@ -1906,14 +2132,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
const entityResolutionPilotEligible = isEntityResolutionPilotEligible(planner);
const livePilotChainSupported = isLivePilotChainSupported(planner.selected_chain_id);
if (!livePilotChainSupported ||
(!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible)) {
const inventoryPilotEligible = isInventoryPilotEligible(planner);
if (!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible &&
!inventoryPilotEligible) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
@ -1946,6 +2172,139 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const organizationScope = organizationScopeForPlanner(planner);
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (inventoryPilotEligible) {
let queryResult = null;
const inventoryIntent = inventoryIntentForPlanner(planner);
const executablePrimitive = inventoryExecutablePrimitiveForPlanner(planner);
if (!inventoryIntent || !executablePrimitive) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_not_mapped");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Inventory exact recipe is not mapped");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Inventory exact recipe is not mapped"],
reason_codes: reasonCodes
};
}
const filters = buildInventoryExactFilters(planner);
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)(inventoryIntent, filters);
if (selection.missing_required_filters.length > 0) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_needs_required_filters");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, `Inventory exact recipe needs required filters: ${selection.missing_required_filters.join(", ")}`);
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: [`Inventory exact recipe needs required filters: ${selection.missing_required_filters.join(", ")}`],
reason_codes: reasonCodes
};
}
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Inventory exact recipe is not available");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Inventory exact recipe is not available"],
reason_codes: reasonCodes
};
}
pushReason(reasonCodes, "pilot_inventory_exact_recipe_selected");
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== executablePrimitive) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, `pilot_inventory_exact_bridge_executes_${executablePrimitive}`));
continue;
}
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_inventory_exact_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_inventory_exact_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeInventoryRows(queryResult) : null;
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildInventoryConfirmedFacts(queryResult, planner, inventoryIntent) : [],
inferredFacts: queryResult ? buildInventoryInferredFacts(queryResult, inventoryIntent) : [],
unknownFacts: buildInventoryUnknownFacts(queryResult, inventoryIntent, toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope)),
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: "inventory_route_template_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: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
if (metadataPilotEligible) {
let metadataResult = null;
const metadataScope = metadataScopeForPlanner(planner);

View File

@ -391,7 +391,13 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
}
if (isInventoryTemplatePilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По exact inventory runtime в 1С найдены подтвержденные строки; ответ ограничен проверенным складским/товарным срезом.";
}
if (isInventoryTemplatePilot(pilot) && mode === "checked_sources_only") {
if (pilot.mcp_execution_performed) {
return "Exact inventory runtime был проверен, но подтвержденный складской/товарный факт в найденных строках не получен.";
}
return "Инвентарный route-template уже выбран, но live-исполнение этого generic MCP контура еще не подключено; складской/товарный факт не подтвержден.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
@ -532,6 +538,9 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "checked_sources_only" && isInventoryTemplatePilot(pilot)) {
if (pilot.mcp_execution_performed) {
return "Можно уточнить дату, организацию, склад, поставщика или позицию и повторить exact inventory проверку.";
}
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
@ -589,8 +598,11 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
}
if (isInventoryTemplatePilot(pilot)) {
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
if (!pilot.mcp_execution_performed) {
claims.push("Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.");
}
claims.push("Do not expose inventory_route_template_v1 or MCP primitive names in the user answer.");
claims.push("Do not claim full inventory coverage outside the checked rows, date, organization, item, or supplier scope.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");

View File

@ -337,6 +337,27 @@ function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "p
return {};
}
function asOfDateFromDateScope(dateScope: string | null): string | null {
if (!dateScope) {
return null;
}
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (dateMatch) {
return `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
}
const monthMatch = dateScope.match(/^(\d{4})-(\d{2})$/);
if (monthMatch) {
const year = Number(monthMatch[1]);
const month = Number(monthMatch[2]);
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`;
}
}
const yearMatch = dateScope.match(/^(\d{4})$/);
return yearMatch ? `${yearMatch[1]}-12-31` : null;
}
function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
@ -365,6 +386,37 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A
};
}
function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const subject = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
const asOfDate = asOfDateFromDateScope(dateScope);
const filters: AddressFilterSet = {
...dateScopeToFilters(dateScope),
...(asOfDate ? { as_of_date: asOfDate } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
if (
planner.selected_chain_id === "inventory_purchase_provenance" ||
planner.selected_chain_id === "inventory_sale_trace"
) {
return {
...filters,
...(subject ? { item: subject } : {})
};
}
if (planner.selected_chain_id === "inventory_supplier_overlap") {
return {
...filters,
...(subject ? { counterparty: subject } : {})
};
}
return filters;
}
function organizationScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
@ -611,6 +663,15 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
);
}
function isInventoryPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
return (
planner.selected_chain_id === "inventory_stock_snapshot" ||
planner.selected_chain_id === "inventory_supplier_overlap" ||
planner.selected_chain_id === "inventory_purchase_provenance" ||
planner.selected_chain_id === "inventory_sale_trace"
);
}
function isMetadataPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
if (
planner.selected_chain_id === "metadata_inspection" ||
@ -766,6 +827,36 @@ function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): V
};
}
function inventoryIntentForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AddressIntent | null {
switch (planner.selected_chain_id) {
case "inventory_stock_snapshot":
return "inventory_on_hand_as_of_date";
case "inventory_supplier_overlap":
return "inventory_supplier_stock_overlap_as_of_date";
case "inventory_purchase_provenance":
return "inventory_purchase_provenance_for_item";
case "inventory_sale_trace":
return "inventory_sale_trace_for_item";
default:
return null;
}
}
function inventoryExecutablePrimitiveForPlanner(
planner: AssistantMcpDiscoveryPlannerContract
): AssistantMcpDiscoveryRuntimeStepContract["primitive_id"] | null {
switch (planner.selected_chain_id) {
case "inventory_stock_snapshot":
case "inventory_supplier_overlap":
return "query_movements";
case "inventory_purchase_provenance":
case "inventory_sale_trace":
return "query_documents";
default:
return null;
}
}
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: step.primitive_id,
@ -1032,6 +1123,16 @@ function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryR
return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`;
}
function summarizeInventoryRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP inventory exact rows fetched";
}
return `${result.fetched_rows} MCP inventory exact rows fetched, ${result.matched_rows} matched inventory scope`;
}
function summarizeMetadataRows(result: AddressMcpMetadataRowsResult): string | null {
if (result.error) {
return null;
@ -1741,6 +1842,55 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
return null;
}
function rowNumberValue(row: Record<string, unknown>, keys: string[]): number | null {
for (const key of keys) {
const candidate = row[key];
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 rowTextValue(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 rowInventoryItemValue(row: Record<string, unknown>): string | null {
return rowTextValue(row, ["Номенклатура", "Item", "item", "Товар", "Product", "product"]);
}
function rowWarehouseValue(row: Record<string, unknown>): string | null {
return rowTextValue(row, ["Склад", "Warehouse", "warehouse"]);
}
function rowDocumentValue(row: Record<string, unknown>): string | null {
return rowTextValue(row, ["Регистратор", "Registrator", "registrator", "Документ", "Document", "document"]);
}
function rowQuantityValue(row: Record<string, unknown>): number | null {
return rowNumberValue(row, ["Количество", "Quantity", "quantity", "Qty", "qty", "Остаток", "Balance", "balance"]);
}
function rowCounterpartyValue(row: Record<string, unknown>): string | null {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
@ -2219,6 +2369,130 @@ function buildBidirectionalValueFlowConfirmedFacts(
];
}
function inventoryLabelRu(intent: AddressIntent): string {
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
return "связи поставщиков с товарным остатком";
}
if (intent === "inventory_purchase_provenance_for_item") {
return "закупочной истории позиции";
}
if (intent === "inventory_sale_trace_for_item") {
return "продаж по позиции";
}
return "складского среза";
}
function inventoryScopeSuffixRu(input: {
intent: AddressIntent;
item: string | null;
counterparty: string | null;
organization: string | null;
asOfDate: string | null;
dateScope: string | null;
}): string {
const parts: string[] = [];
if (input.organization) {
parts.push(`по организации ${input.organization}`);
}
if (input.item) {
parts.push(`по позиции ${input.item}`);
}
if (input.intent === "inventory_supplier_stock_overlap_as_of_date" && input.counterparty) {
parts.push(`по поставщику/контрагенту ${input.counterparty}`);
}
if (input.asOfDate) {
parts.push(`на ${input.asOfDate}`);
} else if (input.dateScope) {
parts.push(`за ${input.dateScope}`);
}
return parts.length > 0 ? ` ${parts.join(", ")}` : "";
}
function inventoryRowSample(row: Record<string, unknown>): string | null {
const item = rowInventoryItemValue(row);
const quantity = rowQuantityValue(row);
const warehouse = rowWarehouseValue(row);
const counterparty = rowCounterpartyValue(row);
const document = rowDocumentValue(row);
const parts: string[] = [];
if (item) {
parts.push(item);
}
if (quantity !== null) {
parts.push(`${quantity} шт.`);
}
if (warehouse) {
parts.push(`склад ${warehouse}`);
}
if (counterparty) {
parts.push(`контрагент ${counterparty}`);
}
if (document) {
parts.push(`документ ${document}`);
}
return parts.length > 0 ? parts.join(", ") : null;
}
function buildInventoryConfirmedFacts(
result: AddressMcpQueryExecutorResult,
planner: AssistantMcpDiscoveryPlannerContract,
intent: AddressIntent
): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const item =
intent === "inventory_purchase_provenance_for_item" || intent === "inventory_sale_trace_for_item"
? firstEntityCandidate(planner)
: null;
const counterparty = intent === "inventory_supplier_stock_overlap_as_of_date" ? firstEntityCandidate(planner) : null;
const scope = inventoryScopeSuffixRu({
intent,
item,
counterparty,
organization: organizationScopeForPlanner(planner),
asOfDate: asOfDateFromDateScope(dateScope),
dateScope
});
const samples = result.rows
.slice(0, 3)
.map((row) => inventoryRowSample(row))
.filter((value): value is string => Boolean(value));
const sampleSuffix = samples.length > 0 ? ` Примеры строк: ${samples.join("; ")}.` : "";
return [`В 1С найдены подтвержденные строки ${inventoryLabelRu(intent)}${scope}: ${result.matched_rows}.${sampleSuffix}`];
}
function buildInventoryInferredFacts(result: AddressMcpQueryExecutorResult, intent: AddressIntent): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
}
if (result.matched_rows <= 0) {
return [
`По ${inventoryLabelRu(intent)} удалось проверить только ограниченный срез 1С; подтвержденных строк этим поиском не найдено.`
];
}
return [
`Вывод по ${inventoryLabelRu(intent)} ограничен найденными строками 1С и указанными датой, организацией, позицией или поставщиком.`
];
}
function buildInventoryUnknownFacts(
result: AddressMcpQueryExecutorResult | null,
intent: AddressIntent,
dateScope: string | null
): string[] {
const facts = [
dateScope
? `Полный товарный контур вне проверенного среза ${dateScope} не подтвержден.`
: "Полный товарный контур без явного проверенного периода или даты не подтвержден."
];
if (!result || result.error || result.matched_rows <= 0) {
facts.unshift(`Подтвержденный факт по ${inventoryLabelRu(intent)} в проверенных строках 1С не найден.`);
}
return facts;
}
function buildLifecycleInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
@ -2441,18 +2715,6 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As
}
}
function isLivePilotChainSupported(chainId: AssistantMcpDiscoveryChainId): boolean {
if (
chainId === "inventory_stock_snapshot" ||
chainId === "inventory_supplier_overlap" ||
chainId === "inventory_purchase_provenance" ||
chainId === "inventory_sale_trace"
) {
return false;
}
return true;
}
export async function executeAssistantMcpDiscoveryPilot(
planner: AssistantMcpDiscoveryPlannerContract,
deps: AssistantMcpDiscoveryPilotExecutorDeps = DEFAULT_DEPS
@ -2525,16 +2787,16 @@ export async function executeAssistantMcpDiscoveryPilot(
const lifecyclePilotEligible = isLifecyclePilotEligible(planner);
const valueFlowPilotEligible = isValueFlowPilotEligible(planner);
const entityResolutionPilotEligible = isEntityResolutionPilotEligible(planner);
const livePilotChainSupported = isLivePilotChainSupported(planner.selected_chain_id);
const inventoryPilotEligible = isInventoryPilotEligible(planner);
if (
!livePilotChainSupported ||
(!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible)
!metadataPilotEligible &&
!documentPilotEligible &&
!movementPilotEligible &&
!lifecyclePilotEligible &&
!valueFlowPilotEligible &&
!entityResolutionPilotEligible &&
!inventoryPilotEligible
) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
@ -2570,6 +2832,152 @@ export async function executeAssistantMcpDiscoveryPilot(
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (inventoryPilotEligible) {
let queryResult: AddressMcpQueryExecutorResult | null = null;
const inventoryIntent = inventoryIntentForPlanner(planner);
const executablePrimitive = inventoryExecutablePrimitiveForPlanner(planner);
if (!inventoryIntent || !executablePrimitive) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_not_mapped");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Inventory exact recipe is not mapped");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Inventory exact recipe is not mapped"],
reason_codes: reasonCodes
};
}
const filters = buildInventoryExactFilters(planner);
const selection = selectAddressRecipe(inventoryIntent, filters);
if (selection.missing_required_filters.length > 0) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_needs_required_filters");
const evidence = buildEmptyEvidence(
planner,
dryRun,
probeResults,
`Inventory exact recipe needs required filters: ${selection.missing_required_filters.join(", ")}`
);
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: [`Inventory exact recipe needs required filters: ${selection.missing_required_filters.join(", ")}`],
reason_codes: reasonCodes
};
}
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_inventory_exact_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Inventory exact recipe is not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "inventory_route_template_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: ["Inventory exact recipe is not available"],
reason_codes: reasonCodes
};
}
pushReason(reasonCodes, "pilot_inventory_exact_recipe_selected");
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== executablePrimitive) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, `pilot_inventory_exact_bridge_executes_${executablePrimitive}`));
continue;
}
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_inventory_exact_mcp_error");
} else {
pushReason(reasonCodes, "pilot_inventory_exact_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeInventoryRows(queryResult) : null;
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildInventoryConfirmedFacts(queryResult, planner, inventoryIntent) : [],
inferredFacts: queryResult ? buildInventoryInferredFacts(queryResult, inventoryIntent) : [],
unknownFacts: buildInventoryUnknownFacts(
queryResult,
inventoryIntent,
toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope)
),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "inventory_route_template_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: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
if (metadataPilotEligible) {
let metadataResult: AddressMcpMetadataRowsResult | null = null;
const metadataScope = metadataScopeForPlanner(planner);

View File

@ -293,7 +293,11 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only");
});
it("keeps inventory catalog templates unsupported until exact runtime evidence is bridged", async () => {
it("bridges inventory stock catalog templates through exact runtime evidence", async () => {
const deps = buildDeps([
{ Period: "2021-09-30T00:00:00", Item: "Widget A", Quantity: 10, Warehouse: "Main" },
{ Period: "2021-09-30T00:00:00", Item: "Widget B", Quantity: 4, Warehouse: "Reserve" }
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
@ -322,7 +326,7 @@ describe("assistant MCP discovery runtime bridge", () => {
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2021-09-30"
},
deps: buildDeps([])
deps
});
const userFacing = [
result.answer_draft.headline,
@ -333,12 +337,14 @@ describe("assistant MCP discovery runtime bridge", () => {
result.answer_draft.next_step_line ?? ""
].join("\n");
expect(result.bridge_status).toBe("unsupported");
expect(result.answer_draft.answer_mode).toBe("checked_sources_only");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(result.pilot.pilot_scope).toBe("inventory_route_template_v1");
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.pilot.executed_primitives).toEqual(["query_movements"]);
expect(deps.executeAddressMcpQuery).toHaveBeenCalledWith(expect.objectContaining({ account_scope: ["41.01"] }));
expect(result.loop_state).toMatchObject({
loop_status: "blocked",
loop_status: "ready_for_next_hop",
selected_chain_id: "inventory_stock_snapshot",
pilot_scope: "inventory_route_template_v1",
asked_domain_family: "inventory_stock",
@ -346,21 +352,126 @@ describe("assistant MCP discovery runtime bridge", () => {
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2021-09-30"
});
expect(result.business_fact_answer_allowed).toBe(false);
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.reason_codes).toContain("pilot_inventory_exact_recipe_selected");
expect(result.reason_codes).toContain("pilot_inventory_exact_mcp_executed");
expect(result.reason_codes).not.toContain("pilot_scope_unsupported_for_live_execution");
expect(result.answer_draft.must_not_claim).toEqual(
expect.arrayContaining([
"Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence.",
"Do not expose inventory_route_template_v1 or MCP primitive names in the user answer."
"Do not expose inventory_route_template_v1 or MCP primitive names in the user answer.",
"Do not claim full inventory coverage outside the checked rows, date, organization, item, or supplier scope."
])
);
expect(userFacing).toContain("Инвентарный route-template");
expect(result.answer_draft.must_not_claim).not.toContain(
"Do not present inventory route-template planning as executed stock, supplier, purchase, or sale evidence."
);
expect(userFacing).toContain("exact inventory runtime");
expect(userFacing).toContain("Widget A");
expect(userFacing).not.toContain("inventory_route_template_v1");
expect(userFacing).not.toContain("query_movements");
expect(userFacing).not.toContain("primitive");
expect(userFacing).not.toContain("MCP discovery pilot");
});
it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
const deps = buildDeps([
{
Period: "2021-08-10T00:00:00",
Item: "Widget A",
Counterparty: "Supplier One",
Registrator: "Purchase 0001"
}
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["Widget A"],
business_fact_family: "inventory_purchase_provenance",
action_family: "purchase_provenance",
aggregation_need: null,
time_scope_need: null,
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [
"resolve_entity_reference",
"fetch_scoped_documents",
"drilldown_related_objects",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unproven_supplier_attribution"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_inventory_purchase_provenance"]
},
turnMeaning: {
asked_domain_family: "inventory_stock",
asked_action_family: "purchase_provenance",
explicit_entity_candidates: ["Widget A"]
},
deps
});
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.inference_lines,
...result.answer_draft.unknown_lines,
...result.answer_draft.limitation_lines,
result.answer_draft.next_step_line ?? ""
].join("\n");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("inventory_purchase_provenance");
expect(result.pilot.executed_primitives).toEqual(["query_documents"]);
expect(deps.executeAddressMcpQuery).toHaveBeenCalledWith(expect.objectContaining({ account_scope: ["41.01"] }));
expect(userFacing).toContain("Widget A");
expect(userFacing).toContain("Supplier One");
expect(userFacing).not.toContain("inventory_route_template_v1");
expect(userFacing).not.toContain("query_documents");
expect(userFacing).not.toContain("primitive");
});
it("keeps selected-item inventory templates in clarification when the item anchor is missing", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "inventory_sale_trace",
action_family: "sale_trace",
aggregation_need: null,
time_scope_need: null,
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["item"],
decomposition_candidates: [
"resolve_entity_reference",
"fetch_scoped_documents",
"drilldown_related_objects",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unproven_buyer_or_sale_trace"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_inventory_sale_trace"]
},
turnMeaning: {
asked_domain_family: "inventory_stock",
asked_action_family: "sale_trace"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.pilot_scope).toBe("inventory_route_template_v1");
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
expect(result.loop_state.pending_axes).toContain("item");
});
it("keeps document evidence executable when the planner expands primitives from fact-axis search", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {