Compare commits

...

10 Commits

36 changed files with 4301 additions and 7 deletions

View File

@ -20,6 +20,13 @@
- `Необходимость жирного ручного прогона` must reflect how strongly the current change still needs a broad manual reality-check beyond unit tests, narrow replay, and build verification.
- The percentages must be honest, architecture-aware, and useful for deciding whether the current pass is safe enough to trust without additional human validation.
## development_stage_progress_rule
- After every completed development stage, always report `Прогресс модуля: X%`.
- The percentage must describe progress inside the current large module or plan block, not the whole project.
- If the stage belongs to a named large block, include that block name next to the percentage.
- Use an integer `0%` to `100%` scale and keep the estimate architecture-aware, based on implemented runtime wiring, tests, replay coverage, and remaining integration risk.
- Do not inflate progress because unit tests are green; semantic replay and real runtime wiring still count as unfinished work when they are pending.
## graphify
This project has a graphify knowledge graph at graphify-out/.

View File

@ -682,6 +682,245 @@ Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts` passed 6/6;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Catalog Index Seed
The second implementation slice of Big Block 5 added the first machine-readable catalog brain for guarded MCP discovery:
- `assistantMcpCatalogIndex.ts`
- `assistantMcpCatalogIndex.test.ts`
The catalog does not execute 1C/MCP calls yet.
It records what each reviewed discovery primitive is allowed to mean:
- business purpose;
- required grounding axes;
- optional axes;
- output fact kinds;
- minimum evidence floor.
This gives future planner/executor wiring a deterministic policy surface:
- every discovery primitive must have a catalog contract;
- a discovery plan can be reviewed against catalog-required axes before execution;
- missing axes degrade to `needs_more_axes` instead of a blind query;
- blocked discovery plans remain blocked at catalog review level.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts` passed 11/11;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Planner Seed
The third implementation slice of Big Block 5 added a standalone planner over the discovery policy and catalog index:
- `assistantMcpDiscoveryPlanner.ts`
- `assistantMcpDiscoveryPlanner.test.ts`
The planner still does not execute live MCP calls.
It converts current-turn meaning into a guarded discovery plan and immediately reviews it against catalog constraints:
- value-flow questions choose entity resolution, movement query, aggregation, and coverage probe;
- document questions choose entity resolution, document query, and coverage probe;
- lifecycle/activity-duration questions choose document evidence plus explicit evidence-basis explanation;
- metadata questions stay in metadata inspection;
- unclassified turns remain in clarification state rather than executing a blind query.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts` passed 17/17;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Runtime Dry-Run Adapter
The fourth implementation slice of Big Block 5 added a dry-run runtime adapter:
- `assistantMcpDiscoveryRuntimeAdapter.ts`
- `assistantMcpDiscoveryRuntimeAdapter.test.ts`
This adapter still does not execute live MCP calls.
It turns planner output into an execution package that future live wiring can consume safely:
- ordered execution steps;
- primitive purpose and expected fact kinds;
- provided and missing grounding axes;
- evidence floor and stop condition per primitive;
- execution budget;
- mandatory evidence gate inputs;
- user-facing fallback for missing scope or policy blocks.
The contract explicitly records `mcp_execution_performed=false`, so this block cannot accidentally query 1C.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts` passed 21/21;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Pilot Executor
The fifth implementation slice of Big Block 5 added the first isolated execution pilot:
- `assistantMcpDiscoveryPilotExecutor.ts`
- `assistantMcpDiscoveryPilotExecutor.test.ts`
This pilot is still not wired into the hot user-facing assistant runtime.
It can execute only one intentionally narrow primitive path:
- `counterparty_lifecycle_query_documents_v1`
- lifecycle/activity-duration turn meaning;
- `query_documents` through injected MCP dependencies;
- all other planned primitives remain skipped in the pilot until dedicated executors exist.
The pilot preserves the safety rules from the previous layers:
- it will not execute when dry-run status needs clarification;
- it will not execute unsupported ready plans;
- MCP errors become limitations, not facts;
- legal registration age remains an unknown fact;
- business activity duration may only be treated as a bounded inference from confirmed 1C activity rows.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts` passed 25/25;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Answer Adapter
The sixth implementation slice of Big Block 5 added a human-safe answer draft adapter:
- `assistantMcpDiscoveryAnswerAdapter.ts`
- `assistantMcpDiscoveryAnswerAdapter.test.ts`
This adapter is still not wired into the hot assistant runtime.
It converts pilot evidence into an answer draft that can later be consumed by the final answer layer:
- confirmed lines;
- bounded inference lines;
- unknown fact boundaries;
- user-facing limitations;
- next-step guidance;
- `must_not_claim` constraints.
The adapter explicitly blocks internal mechanics from user-facing lines:
- MCP primitive names;
- query text;
- debug/reason mechanics;
- raw runtime/planner/catalog codes.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts` passed 29/29;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Runtime Bridge
The seventh implementation slice of Big Block 5 added a runtime bridge contract:
- `assistantMcpDiscoveryRuntimeBridge.ts`
- `assistantMcpDiscoveryRuntimeBridge.test.ts`
This bridge is still not wired into the hot assistant answer path.
It composes the already separated discovery layers into one orchestration-facing contract:
- planner;
- isolated pilot executor;
- human-safe answer draft;
- bridge status;
- explicit `hot_runtime_wired=false`;
- business-fact answer authorization flags;
- clarification and unsupported-state flags.
This gives future runtime integration a single object to attach to debug/evidence flow without letting discovery silently become a user-facing answer path.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 33/33;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Turn Input Adapter
The eighth implementation slice of Big Block 5 added the adapter between current-turn meaning/predecompose contracts and MCP discovery input:
- `assistantMcpDiscoveryTurnInputAdapter.ts`
- `assistantMcpDiscoveryTurnInputAdapter.test.ts`
This adapter still does not wire discovery into the hot answer path.
It solves the next runtime integration seam:
- maps `assistant_turn_meaning_v1` into `AssistantMcpDiscoveryTurnMeaningRef`;
- extracts counterparty, organization, and date scope from predecompose contracts;
- bootstraps lifecycle/activity-duration questions from raw user wording;
- treats unsupported-but-understood meaning as discovery-eligible instead of stale-replay fallback;
- avoids serializing structured entity candidates as `[object Object]`;
- keeps supported exact routes out of MCP discovery.
Validation:
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 37/37;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Runtime Entry Point
The ninth implementation slice of Big Block 5 added a runtime entry point:
- `assistantMcpDiscoveryRuntimeEntryPoint.ts`
- `assistantMcpDiscoveryRuntimeEntryPoint.test.ts`
This entry point is still not wired into the hot assistant answer path.
It gives runtime integration a single safe call boundary:
- builds discovery turn input from assistant turn meaning, predecompose contract, and raw wording;
- skips supported exact turns before any discovery execution;
- executes the runtime bridge only for discovery-eligible turns;
- keeps `hot_runtime_wired=false`;
- exposes `discovery_attempted`, `entry_status`, `turn_input`, and optional `bridge`.
This is the first complete non-hot pipeline from current-turn context to guarded MCP discovery result.
Validation:
- `npm test -- assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 40/40;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Debug Attachment
The tenth implementation slice of Big Block 5 added a debug/evidence attachment layer:
- `assistantMcpDiscoveryDebugAttachment.ts`
- `assistantMcpDiscoveryDebugAttachment.test.ts`
It is now attached to:
- address-lane response runtime debug;
- deep-analysis debug payloads.
This still does not execute MCP discovery automatically and still does not change the user-facing answer.
It gives runtime and replay review a stable observable surface:
- `assistant_mcp_discovery_entry_point_v1`;
- `mcp_discovery_entry_status`;
- `mcp_discovery_attempted`;
- `mcp_discovery_hot_runtime_wired=false`;
- `mcp_discovery_bridge_status`;
- `mcp_discovery_answer_mode`;
- business-fact/user-facing authorization flags;
- clarification flag.
Validation:
- `npm test -- assistantMcpDiscoveryDebugAttachment.test.ts assistantDebugPayloadAssembler.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts` passed 13/13;
- `npm run build` passed.
## Execution Rule
Do not implement this plan as:

View File

@ -4,6 +4,7 @@ exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponse
const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter");
const assistantCapabilityBindingResponseGuard_1 = require("./assistantCapabilityBindingResponseGuard");
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
@ -208,13 +209,16 @@ function runAssistantAddressLaneResponseRuntime(input) {
addressRuntimeMeta: input.llmPreDecomposeMeta,
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
});
const debugWithMcpDiscovery = (0, assistantMcpDiscoveryDebugAttachment_1.attachAssistantMcpDiscoveryDebug)(debugWithCapabilityBinding, {
addressRuntimeMeta: input.llmPreDecomposeMeta
});
const guardedResponse = (0, assistantCapabilityBindingResponseGuard_1.applyAssistantCapabilityBindingResponseGuard)({
assistantReply: safeAddressReply,
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
capabilityBinding: debugWithCapabilityBinding.assistant_capability_binding_v1
capabilityBinding: debugWithMcpDiscovery.assistant_capability_binding_v1
});
const debugWithResponseGuard = {
...debugWithCapabilityBinding,
...debugWithMcpDiscovery,
capability_binding_response_guard: guardedResponse.audit
};
const finalization = finalizeAddressTurnSafe({

View File

@ -5,6 +5,7 @@ exports.buildAssistantBackendErrorDebugPayload = buildAssistantBackendErrorDebug
exports.buildAddressRuntimeDebugPayload = buildAddressRuntimeDebugPayload;
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
@ -240,6 +241,7 @@ function buildDeepAnalysisDebugPayload(input) {
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
address_semantic_extraction_contract: input.addressRuntimeMetaForDeep?.semanticExtractionContract ?? null,
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
assistant_mcp_discovery_entry_point_v1: input.addressRuntimeMetaForDeep?.mcpDiscoveryRuntimeEntryPoint ?? null,
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_contract_stage4_v1: answerContractStage4Audit,
@ -263,10 +265,13 @@ function buildDeepAnalysisDebugPayload(input) {
coverageReport: input.coverageReport,
replyType: "deep_analysis"
});
return (0, assistantCapabilityRuntimeBindingAdapter_1.attachAssistantCapabilityRuntimeBinding)(debugWithStateTransition, {
const debugWithCapabilityBinding = (0, assistantCapabilityRuntimeBindingAdapter_1.attachAssistantCapabilityRuntimeBinding)(debugWithStateTransition, {
addressRuntimeMeta: input.addressRuntimeMetaForDeep,
groundingStatus: input.groundingCheck.status,
coverageReport: input.coverageReport,
replyType: "deep_analysis"
});
return (0, assistantMcpDiscoveryDebugAttachment_1.attachAssistantMcpDiscoveryDebug)(debugWithCapabilityBinding, {
addressRuntimeMeta: input.addressRuntimeMetaForDeep
});
}

View File

@ -0,0 +1,203 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1";
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = "assistant_mcp_catalog_plan_review_v1";
const PRIMITIVE_CONTRACTS = [
{
primitive_id: "search_business_entity",
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "document", "account"],
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
evidence_floor: "rows_received",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "inspect_1c_metadata",
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
optional_axes: ["business_entity", "account", "counterparty"],
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
evidence_floor: "source_summary",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "resolve_entity_reference",
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "inn", "document"],
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "query_movements",
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "query_documents",
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
optional_axes: ["account", "amount", "item", "warehouse"],
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "aggregate_by_axis",
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
optional_axes: ["organization", "contract", "document", "amount"],
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "drilldown_related_objects",
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
optional_axes: ["period", "account", "amount"],
output_fact_kinds: ["related_objects", "relationship_edges"],
evidence_floor: "rows_received",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "probe_coverage",
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
optional_axes: ["period", "organization", "counterparty", "document", "account"],
output_fact_kinds: ["coverage_status", "known_gaps"],
evidence_floor: "source_summary",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "explain_evidence_basis",
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
optional_axes: ["coverage_target", "domain_family"],
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
evidence_floor: "source_summary",
safe_for_model_planning: true,
runtime_must_execute: true
}
];
const PRIMITIVE_CONTRACT_MAP = new Map(PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract]));
function toStringSet(values) {
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function hasAnyAxisGroup(axisSet, groups) {
return groups.some((group) => group.every((axis) => axisSet.has(axis)));
}
function missingAxisGroups(axisSet, groups) {
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
}
function buildAssistantMcpCatalogIndex() {
const reasonCodes = [];
const missingContracts = assistantMcpDiscoveryPolicy_1.ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
if (missingContracts.length > 0) {
pushReason(reasonCodes, "catalog_missing_discovery_primitive_contract");
}
else {
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
}
return {
schema_version: exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex",
primitives: PRIMITIVE_CONTRACTS,
reason_codes: reasonCodes
};
}
function getAssistantMcpCatalogPrimitive(primitive) {
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
if (!contract) {
throw new Error(`Missing MCP catalog primitive contract: ${primitive}`);
}
return contract;
}
function reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan) {
const reasonCodes = [];
const axisSet = toStringSet(plan.required_axes);
const reviewedPrimitives = [];
const missingAxesByPrimitive = {};
const unknownPrimitives = [];
const evidenceFloors = {};
for (const primitive of plan.allowed_primitives) {
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
if (!contract) {
unknownPrimitives.push(primitive);
continue;
}
reviewedPrimitives.push(primitive);
evidenceFloors[primitive] = contract.evidence_floor;
if (!hasAnyAxisGroup(axisSet, contract.required_axes_any_of)) {
missingAxesByPrimitive[primitive] = missingAxisGroups(axisSet, contract.required_axes_any_of);
}
}
if (unknownPrimitives.length > 0) {
pushReason(reasonCodes, "catalog_unknown_primitive_in_discovery_plan");
}
if (Object.keys(missingAxesByPrimitive).length > 0) {
pushReason(reasonCodes, "catalog_required_axes_missing_for_primitive");
}
if (plan.plan_status !== "allowed") {
pushReason(reasonCodes, "catalog_review_received_non_allowed_plan");
}
let reviewStatus = "catalog_compatible";
if (unknownPrimitives.length > 0 || plan.plan_status === "blocked") {
reviewStatus = "catalog_blocked";
}
else if (plan.plan_status !== "allowed" || Object.keys(missingAxesByPrimitive).length > 0) {
reviewStatus = "needs_more_axes";
}
if (reviewStatus === "catalog_compatible") {
pushReason(reasonCodes, "catalog_plan_compatible");
}
else if (reviewStatus === "catalog_blocked") {
pushReason(reasonCodes, "catalog_plan_blocked");
}
else {
pushReason(reasonCodes, "catalog_plan_needs_more_axes");
}
return {
schema_version: exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex",
review_status: reviewStatus,
reviewed_primitives: reviewedPrimitives,
missing_axes_by_primitive: missingAxesByPrimitive,
unknown_primitives: unknownPrimitives,
evidence_floors: evidenceFloors,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,124 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values) {
const result = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function isInternalMechanicsLine(value) {
const text = value.toLowerCase();
return (text.includes("primitive") ||
text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("resolve_entity_reference") ||
text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_"));
}
function userFacingLimitations(values) {
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
}
function modeFor(pilot) {
if (pilot.pilot_status === "blocked") {
return "blocked";
}
if (pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.evidence.answer_permission === "confirmed_answer") {
return "confirmed_with_bounded_inference";
}
if (pilot.evidence.answer_permission === "bounded_inference") {
return "bounded_inference_only";
}
return "checked_sources_only";
}
function headlineFor(mode) {
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
if (mode === "blocked") {
return "Поиск в 1С заблокирован runtime-политикой до выполнения.";
}
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
}
function nextStepFor(mode, pilot) {
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
}
if (mode === "blocked") {
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
}
return null;
}
function buildMustNotClaim(pilot) {
const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false."
];
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
return claims;
}
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
pushReason(reasonCodes, `answer_mode_${mode}`);
if (pilot.evidence.unknown_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
}
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode,
headline: headlineFor(mode),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
inference_lines: uniqueStrings(pilot.evidence.inferred_facts),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
next_step_line: nextStepFor(mode, pilot),
internal_mechanics_allowed: false,
must_not_claim: buildMustNotClaim(pilot),
reason_codes: uniqueStrings(reasonCodes)
};
}

View File

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantMcpDiscoveryDebugAttachmentFields = buildAssistantMcpDiscoveryDebugAttachmentFields;
exports.attachAssistantMcpDiscoveryDebug = attachAssistantMcpDiscoveryDebug;
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function isMcpDiscoveryEntryPointContract(value) {
const record = toRecordObject(value);
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
}
function resolveEntryPoint(input) {
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
return input.entryPoint;
}
const runtimeMetaEntryPoint = input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
}
function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
const entryPoint = resolveEntryPoint(input);
const bridge = toRecordObject(entryPoint?.bridge);
const answerDraft = toRecordObject(bridge?.answer_draft);
return {
assistant_mcp_discovery_entry_point_v1: entryPoint,
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
mcp_discovery_hot_runtime_wired: false,
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
mcp_discovery_requires_clarification: bridge?.requires_user_clarification === true
};
}
function attachAssistantMcpDiscoveryDebug(debugPayload, input) {
return {
...debugPayload,
...buildAssistantMcpDiscoveryDebugAttachmentFields(input)
};
}

View File

@ -0,0 +1,286 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = void 0;
exports.executeAssistantMcpDiscoveryPilot = executeAssistantMcpDiscoveryPilot;
const addressMcpClient_1 = require("./addressMcpClient");
const assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter");
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
};
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target, value) {
const text = value.trim();
if (text && !target.includes(text)) {
target.push(text);
}
}
function firstEntityCandidate(planner) {
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function dateScopeToFilters(dateScope) {
if (!dateScope) {
return {};
}
const yearMatch = dateScope.match(/^(\d{4})$/);
if (yearMatch) {
return {
period_from: `${yearMatch[1]}-01-01`,
period_to: `${yearMatch[1]}-12-31`
};
}
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (dateMatch) {
const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
return {
period_from: date,
period_to: date
};
}
return {};
}
function buildLifecycleFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const combined = `${domain} ${action}`;
return (planner.proposed_primitives.includes("query_documents") &&
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age")));
}
function skippedProbeResult(step, limitation) {
return {
primitive_id: step.primitive_id,
status: "skipped",
rows_received: 0,
rows_matched: 0,
limitation
};
}
function queryResultToProbeResult(primitiveId, result) {
return {
primitive_id: primitiveId,
status: result.error ? "error" : "ok",
rows_received: result.fetched_rows,
rows_matched: result.matched_rows,
limitation: result.error
};
}
function summarizeRows(result) {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP document rows fetched";
}
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
}
function buildConfirmedFacts(result, counterparty) {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C activity rows were found for counterparty ${counterparty}`
: "1C activity rows were found for the requested counterparty scope"
];
}
function buildInferredFacts(result) {
if (result.error || result.fetched_rows <= 0) {
return [];
}
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
}
function buildUnknownFacts() {
return ["Legal registration date is not proven by this MCP discovery pilot"];
}
function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
return (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
unknownFacts: [reason],
queryLimitations: [reason],
recommendedNextProbe: dryRun.user_facing_fallback
});
}
async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const dryRun = (0, assistantMcpDiscoveryRuntimeAdapter_1.buildAssistantMcpDiscoveryRuntimeDryRun)(planner);
const reasonCodes = [...dryRun.reason_codes];
const executedPrimitives = [];
const skippedPrimitives = [];
const probeResults = [];
const queryLimitations = [];
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");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "blocked",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes
};
}
if (dryRun.adapter_status !== "dry_run_ready") {
pushReason(reasonCodes, "pilot_needs_clarification_before_mcp_execution");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot needs more scope before execution");
return {
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",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes
};
}
if (!isLifecyclePilotEligible(planner)) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_scope_unsupported_for_live_execution"));
}
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot scope is not implemented yet");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes
};
}
let queryResult = null;
const counterparty = firstEntityCandidate(planner);
const filters = buildLifecycleFilters(planner);
const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_lifecycle_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Lifecycle recipe is not available");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_documents") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_documents_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,161 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = void 0;
exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery;
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1";
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function lower(value) {
return String(value ?? "").trim().toLowerCase();
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target, value) {
const text = value.trim();
if (text && !target.includes(text)) {
target.push(text);
}
}
function hasEntity(meaning) {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function addScopeAxes(axes, meaning) {
if (hasEntity(meaning)) {
pushUnique(axes, "counterparty");
}
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
pushUnique(axes, "organization");
}
if (toNonEmptyString(meaning?.explicit_date_scope)) {
pushUnique(axes, "period");
}
}
function includesAny(text, tokens) {
return tokens.some((token) => text.includes(token));
}
function recipeFor(input) {
const meaning = input.turnMeaning ?? null;
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
addScopeAxes(axes, meaning);
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "counterparty value-flow evidence",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: "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");
pushUnique(axes, "evidence_basis");
return {
semanticDataNeed: "counterparty lifecycle evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
};
}
if (includesAny(combined, ["metadata", "schema", "catalog"])) {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
};
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
return {
semanticDataNeed: "entity discovery evidence",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
};
}
return {
semanticDataNeed: "unclassified 1C discovery need",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
};
}
function statusFrom(plan, review) {
if (plan.plan_status === "blocked" || review.review_status === "catalog_blocked") {
return "blocked";
}
if (plan.plan_status !== "allowed" || review.review_status !== "catalog_compatible") {
return "needs_clarification";
}
return "ready_for_execution";
}
function planAssistantMcpDiscovery(input) {
const recipe = recipeFor(input);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const reasonCodes = [];
pushReason(reasonCodes, recipe.reason);
const plan = (0, assistantMcpDiscoveryPolicy_1.buildAssistantMcpDiscoveryPlan)({
semanticDataNeed,
turnMeaning: input.turnMeaning,
proposedPrimitives: recipe.primitives,
requiredAxes: recipe.axes
});
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
const plannerStatus = statusFrom(plan, review);
if (plannerStatus === "ready_for_execution") {
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
}
else if (plannerStatus === "blocked") {
pushReason(reasonCodes, "planner_blocked_by_policy_or_catalog");
}
else {
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: review,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,129 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryRuntimeDryRun = buildAssistantMcpDiscoveryRuntimeDryRun;
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_dry_run_v1";
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values) {
const result = [];
for (const value of values) {
const text = value.trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function stepStatusFor(input) {
if (input.adapterStatus === "blocked") {
return "blocked";
}
if (input.missingAxisOptions.length > 0) {
return "missing_axes";
}
return "ready";
}
function stopConditionFor(evidenceFloor) {
if (evidenceFloor === "rows_matched") {
return "stop_after_allowed_probe_returns_matched_rows_or_reports_no_match";
}
if (evidenceFloor === "rows_received") {
return "stop_after_allowed_probe_returns_rows_or_reports_empty_source";
}
if (evidenceFloor === "source_summary") {
return "stop_after_allowed_probe_returns_source_summary_or_limitation";
}
return "stop_without_fact_claim";
}
function adapterStatusFor(planner) {
if (planner.planner_status === "blocked" || planner.catalog_review.review_status === "catalog_blocked") {
return "blocked";
}
if (planner.planner_status !== "ready_for_execution" || planner.catalog_review.review_status !== "catalog_compatible") {
return "needs_clarification";
}
return "dry_run_ready";
}
function fallbackFor(status) {
if (status === "dry_run_ready") {
return null;
}
if (status === "blocked") {
return "explain_that_runtime_policy_blocked_mcp_discovery";
}
return "ask_for_missing_scope_before_mcp_discovery";
}
function buildAssistantMcpDiscoveryRuntimeDryRun(planner) {
const adapterStatus = adapterStatusFor(planner);
const reasonCodes = uniqueStrings([
...planner.reason_codes,
...planner.discovery_plan.reason_codes,
...planner.catalog_review.reason_codes
]);
const providedAxes = uniqueStrings(planner.required_axes);
const executionSteps = planner.discovery_plan.allowed_primitives.map((primitiveId, index) => {
const catalog = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogPrimitive)(primitiveId);
const missingAxisOptions = planner.catalog_review.missing_axes_by_primitive[primitiveId] ?? [];
return {
sequence: index + 1,
primitive_id: primitiveId,
step_status: stepStatusFor({ adapterStatus, missingAxisOptions }),
purpose: catalog.purpose,
provided_axes: providedAxes,
required_axis_options: catalog.required_axes_any_of,
missing_axis_options: missingAxisOptions,
optional_axes: catalog.optional_axes,
expected_fact_kinds: catalog.output_fact_kinds,
evidence_floor: catalog.evidence_floor,
runtime_must_execute: true,
dry_run_only: true,
stop_condition: stopConditionFor(catalog.evidence_floor)
};
});
if (adapterStatus === "dry_run_ready") {
pushReason(reasonCodes, "runtime_dry_run_ready_without_mcp_execution");
}
else if (adapterStatus === "blocked") {
pushReason(reasonCodes, "runtime_dry_run_blocked_before_mcp_execution");
}
else {
pushReason(reasonCodes, "runtime_dry_run_needs_clarification_before_mcp_execution");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeAdapter",
adapter_status: adapterStatus,
planner_status: planner.planner_status,
mcp_execution_performed: false,
execution_steps: executionSteps,
execution_budget: planner.discovery_plan.execution_budget,
evidence_gate: {
required: true,
expected_inputs: [
"probe_results",
"confirmed_facts",
"inferred_facts",
"unknown_facts",
"source_rows_summary",
"query_limitations"
],
answer_may_use_raw_model_claims: false
},
user_facing_fallback: fallbackFor(adapterStatus),
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values) {
const result = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function bridgeStatusFor(pilot, draft) {
if (draft.answer_mode === "blocked" || pilot.pilot_status === "blocked") {
return "blocked";
}
if (draft.answer_mode === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.pilot_status === "unsupported") {
return "unsupported";
}
if (draft.answer_mode === "checked_sources_only") {
return "checked_sources_only";
}
return "answer_draft_ready";
}
function businessFactAnswerAllowed(draft) {
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
}
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
semanticDataNeed: input.semanticDataNeed,
turnMeaning: input.turnMeaning
});
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
bridge_status: bridgeStatus,
hot_runtime_wired: false,
planner,
pilot,
answer_draft: answerDraft,
user_facing_response_allowed: bridgeStatus !== "blocked",
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
requires_user_clarification: bridgeStatus === "needs_clarification",
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION = void 0;
exports.runAssistantMcpDiscoveryRuntimeEntryPoint = runAssistantMcpDiscoveryRuntimeEntryPoint;
const assistantMcpDiscoveryRuntimeBridge_1 = require("./assistantMcpDiscoveryRuntimeBridge");
const assistantMcpDiscoveryTurnInputAdapter_1 = require("./assistantMcpDiscoveryTurnInputAdapter");
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_entry_point_v1";
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values) {
const result = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function skippedContract(input) {
const reasonCodes = uniqueStrings(input.turnInput.reason_codes);
pushReason(reasonCodes, input.reason);
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: input.status,
hot_runtime_wired: false,
discovery_attempted: false,
turn_input: input.turnInput,
bridge: null,
reason_codes: reasonCodes
};
}
async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) {
const turnInput = (0, assistantMcpDiscoveryTurnInputAdapter_1.buildAssistantMcpDiscoveryTurnInput)(input);
if (!turnInput.should_run_discovery) {
return skippedContract({
status: "skipped_not_applicable",
turnInput,
reason: "runtime_entry_point_skipped_supported_exact_turn"
});
}
if (!turnInput.turn_meaning_ref) {
return skippedContract({
status: "skipped_needs_more_context",
turnInput,
reason: "runtime_entry_point_skipped_missing_discovery_turn_meaning"
});
}
const bridge = await (0, assistantMcpDiscoveryRuntimeBridge_1.runAssistantMcpDiscoveryRuntimeBridge)({
semanticDataNeed: turnInput.semantic_data_need,
turnMeaning: turnInput.turn_meaning_ref,
deps: input.deps
});
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: turnInput,
bridge,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,215 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target, value) {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function compactLower(value) {
return String(value ?? "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
}
function candidateValue(value) {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text));
}
function collectEntityCandidates(value) {
const result = [];
if (Array.isArray(value)) {
for (const item of value) {
pushUnique(result, candidateValue(item));
}
return result;
}
pushUnique(result, candidateValue(value));
return result;
}
function collectPredecomposeEntities(predecompose) {
const entities = toRecordObject(predecompose?.entities);
return {
counterparty: toNonEmptyString(entities?.counterparty),
organization: toNonEmptyString(entities?.organization)
};
}
function collectDateScope(predecompose) {
const period = toRecordObject(predecompose?.period);
const asOfDate = toNonEmptyString(period?.as_of_date);
const periodFrom = toNonEmptyString(period?.period_from);
const periodTo = toNonEmptyString(period?.period_to);
if (asOfDate) {
return asOfDate;
}
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
return yearFrom[1];
}
if (periodFrom && periodTo) {
return `${periodFrom}..${periodTo}`;
}
return periodFrom ?? periodTo ?? null;
}
function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
}
function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
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.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
return false;
}
function buildAssistantMcpDiscoveryTurnInput(input) {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const reasonCodes = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const semanticDataNeed = semanticNeedFor({
domain: rawDomain,
action: rawAction,
unsupported,
lifecycleSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
const turnMeaning = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
};
const cleanTurnMeaning = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
}
if (toNonEmptyString(turnMeaning.asked_action_family)) {
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
}
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
}
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
}
if (turnMeaning.stale_replay_forbidden) {
cleanTurnMeaning.stale_replay_forbidden = true;
}
const runDiscovery = shouldRunDiscovery({
unsupported,
lifecycleSignal,
semanticDataNeed,
explicitIntentCandidate
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal = assistantTurnMeaning
? "assistant_turn_meaning"
: predecomposeContract
? "predecompose_contract"
: lifecycleSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
if (!runDiscovery) {
pushReason(reasonCodes, "mcp_discovery_not_applicable_for_supported_exact_turn");
}
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes
};
}

View File

@ -9,6 +9,7 @@ import {
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
@ -267,13 +268,16 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
addressRuntimeMeta: input.llmPreDecomposeMeta,
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
});
const debugWithMcpDiscovery = attachAssistantMcpDiscoveryDebug(debugWithCapabilityBinding, {
addressRuntimeMeta: input.llmPreDecomposeMeta
});
const guardedResponse = applyAssistantCapabilityBindingResponseGuard({
assistantReply: safeAddressReply,
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
capabilityBinding: debugWithCapabilityBinding.assistant_capability_binding_v1
capabilityBinding: debugWithMcpDiscovery.assistant_capability_binding_v1
});
const debugWithResponseGuard = {
...debugWithCapabilityBinding,
...debugWithMcpDiscovery,
capability_binding_response_guard: guardedResponse.audit
};
const finalization = finalizeAddressTurnSafe({

View File

@ -24,6 +24,7 @@ import type {
TemporalGuardAudit
} from "./assistantRuntimeGuards";
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
@ -333,6 +334,7 @@ export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInp
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
address_semantic_extraction_contract: input.addressRuntimeMetaForDeep?.semanticExtractionContract ?? null,
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
assistant_mcp_discovery_entry_point_v1: input.addressRuntimeMetaForDeep?.mcpDiscoveryRuntimeEntryPoint ?? null,
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_contract_stage4_v1: answerContractStage4Audit,
@ -356,10 +358,13 @@ export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInp
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
replyType: "deep_analysis"
});
return attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
const debugWithCapabilityBinding = attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined,
groundingStatus: input.groundingCheck.status,
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
replyType: "deep_analysis"
});
return attachAssistantMcpDiscoveryDebug(debugWithCapabilityBinding, {
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined
}) as unknown as AssistantDebugPayload;
}

View File

@ -0,0 +1,251 @@
import {
ASSISTANT_MCP_DISCOVERY_PRIMITIVES,
type AssistantMcpDiscoveryPlanContract,
type AssistantMcpDiscoveryPrimitive
} from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1" as const;
export const ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = "assistant_mcp_catalog_plan_review_v1" as const;
export type AssistantMcpCatalogEvidenceFloor = "none" | "rows_received" | "rows_matched" | "source_summary";
export type AssistantMcpCatalogPlanReviewStatus = "catalog_compatible" | "needs_more_axes" | "catalog_blocked";
export interface AssistantMcpCatalogPrimitiveContract {
primitive_id: AssistantMcpDiscoveryPrimitive;
purpose: string;
required_axes_any_of: string[][];
optional_axes: string[];
output_fact_kinds: string[];
evidence_floor: AssistantMcpCatalogEvidenceFloor;
safe_for_model_planning: true;
runtime_must_execute: true;
}
export interface AssistantMcpCatalogIndexContract {
schema_version: typeof ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION;
policy_owner: "assistantMcpCatalogIndex";
primitives: AssistantMcpCatalogPrimitiveContract[];
reason_codes: string[];
}
export interface AssistantMcpCatalogPlanReview {
schema_version: typeof ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION;
policy_owner: "assistantMcpCatalogIndex";
review_status: AssistantMcpCatalogPlanReviewStatus;
reviewed_primitives: AssistantMcpDiscoveryPrimitive[];
missing_axes_by_primitive: Record<string, string[][]>;
unknown_primitives: string[];
evidence_floors: Record<string, AssistantMcpCatalogEvidenceFloor>;
reason_codes: string[];
}
const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "search_business_entity",
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "document", "account"],
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
evidence_floor: "rows_received",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "inspect_1c_metadata",
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
optional_axes: ["business_entity", "account", "counterparty"],
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
evidence_floor: "source_summary",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "resolve_entity_reference",
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "inn", "document"],
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "query_movements",
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "query_documents",
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
optional_axes: ["account", "amount", "item", "warehouse"],
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "aggregate_by_axis",
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
optional_axes: ["organization", "contract", "document", "amount"],
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
evidence_floor: "rows_matched",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "drilldown_related_objects",
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
optional_axes: ["period", "account", "amount"],
output_fact_kinds: ["related_objects", "relationship_edges"],
evidence_floor: "rows_received",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "probe_coverage",
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
optional_axes: ["period", "organization", "counterparty", "document", "account"],
output_fact_kinds: ["coverage_status", "known_gaps"],
evidence_floor: "source_summary",
safe_for_model_planning: true,
runtime_must_execute: true
},
{
primitive_id: "explain_evidence_basis",
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
optional_axes: ["coverage_target", "domain_family"],
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
evidence_floor: "source_summary",
safe_for_model_planning: true,
runtime_must_execute: true
}
];
const PRIMITIVE_CONTRACT_MAP = new Map<AssistantMcpDiscoveryPrimitive, AssistantMcpCatalogPrimitiveContract>(
PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract])
);
function toStringSet(values: string[]): Set<string> {
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function hasAnyAxisGroup(axisSet: Set<string>, groups: string[][]): boolean {
return groups.some((group) => group.every((axis) => axisSet.has(axis)));
}
function missingAxisGroups(axisSet: Set<string>, groups: string[][]): string[][] {
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
}
export function buildAssistantMcpCatalogIndex(): AssistantMcpCatalogIndexContract {
const reasonCodes: string[] = [];
const missingContracts = ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
if (missingContracts.length > 0) {
pushReason(reasonCodes, "catalog_missing_discovery_primitive_contract");
} else {
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
}
return {
schema_version: ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex",
primitives: PRIMITIVE_CONTRACTS,
reason_codes: reasonCodes
};
}
export function getAssistantMcpCatalogPrimitive(
primitive: AssistantMcpDiscoveryPrimitive
): AssistantMcpCatalogPrimitiveContract {
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
if (!contract) {
throw new Error(`Missing MCP catalog primitive contract: ${primitive}`);
}
return contract;
}
export function reviewAssistantMcpDiscoveryPlanAgainstCatalog(
plan: AssistantMcpDiscoveryPlanContract
): AssistantMcpCatalogPlanReview {
const reasonCodes: string[] = [];
const axisSet = toStringSet(plan.required_axes);
const reviewedPrimitives: AssistantMcpDiscoveryPrimitive[] = [];
const missingAxesByPrimitive: Record<string, string[][]> = {};
const unknownPrimitives: string[] = [];
const evidenceFloors: Record<string, AssistantMcpCatalogEvidenceFloor> = {};
for (const primitive of plan.allowed_primitives) {
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
if (!contract) {
unknownPrimitives.push(primitive);
continue;
}
reviewedPrimitives.push(primitive);
evidenceFloors[primitive] = contract.evidence_floor;
if (!hasAnyAxisGroup(axisSet, contract.required_axes_any_of)) {
missingAxesByPrimitive[primitive] = missingAxisGroups(axisSet, contract.required_axes_any_of);
}
}
if (unknownPrimitives.length > 0) {
pushReason(reasonCodes, "catalog_unknown_primitive_in_discovery_plan");
}
if (Object.keys(missingAxesByPrimitive).length > 0) {
pushReason(reasonCodes, "catalog_required_axes_missing_for_primitive");
}
if (plan.plan_status !== "allowed") {
pushReason(reasonCodes, "catalog_review_received_non_allowed_plan");
}
let reviewStatus: AssistantMcpCatalogPlanReviewStatus = "catalog_compatible";
if (unknownPrimitives.length > 0 || plan.plan_status === "blocked") {
reviewStatus = "catalog_blocked";
} else if (plan.plan_status !== "allowed" || Object.keys(missingAxesByPrimitive).length > 0) {
reviewStatus = "needs_more_axes";
}
if (reviewStatus === "catalog_compatible") {
pushReason(reasonCodes, "catalog_plan_compatible");
} else if (reviewStatus === "catalog_blocked") {
pushReason(reasonCodes, "catalog_plan_blocked");
} else {
pushReason(reasonCodes, "catalog_plan_needs_more_axes");
}
return {
schema_version: ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex",
review_status: reviewStatus,
reviewed_primitives: reviewedPrimitives,
missing_axes_by_primitive: missingAxesByPrimitive,
unknown_primitives: unknownPrimitives,
evidence_floors: evidenceFloors,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,160 @@
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
"assistant_mcp_discovery_answer_draft_v1" as const;
export type AssistantMcpDiscoveryAnswerMode =
| "confirmed_with_bounded_inference"
| "bounded_inference_only"
| "checked_sources_only"
| "needs_clarification"
| "blocked";
export interface AssistantMcpDiscoveryAnswerDraftContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryAnswerAdapter";
answer_mode: AssistantMcpDiscoveryAnswerMode;
headline: string;
confirmed_lines: string[];
inference_lines: string[];
unknown_lines: string[];
limitation_lines: string[];
next_step_line: string | null;
internal_mechanics_allowed: false;
must_not_claim: string[];
reason_codes: string[];
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function isInternalMechanicsLine(value: string): boolean {
const text = value.toLowerCase();
return (
text.includes("primitive") ||
text.includes("query_documents") ||
text.includes("query_movements") ||
text.includes("resolve_entity_reference") ||
text.includes("probe_coverage") ||
text.includes("explain_evidence_basis") ||
text.includes("pilot_only_executes") ||
text.includes("runtime_") ||
text.includes("planner_") ||
text.includes("catalog_")
);
}
function userFacingLimitations(values: string[]): string[] {
return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value));
}
function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantMcpDiscoveryAnswerMode {
if (pilot.pilot_status === "blocked") {
return "blocked";
}
if (pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.evidence.answer_permission === "confirmed_answer") {
return "confirmed_with_bounded_inference";
}
if (pilot.evidence.answer_permission === "bounded_inference") {
return "bounded_inference_only";
}
return "checked_sources_only";
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode): string {
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
if (mode === "blocked") {
return "Поиск в 1С заблокирован runtime-политикой до выполнения.";
}
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
}
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
}
if (mode === "blocked") {
return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя.";
}
return null;
}
function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const claims = [
"Do not claim legal registration age unless a legal registration source is confirmed.",
"Do not present inferred activity duration as a formally confirmed legal fact.",
"Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.",
"Do not claim rows were checked when mcp_execution_performed=false."
];
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
return claims;
}
export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract {
const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
pushReason(reasonCodes, `answer_mode_${mode}`);
if (pilot.evidence.unknown_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_unknown_fact_boundary");
}
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
answer_mode: mode,
headline: headlineFor(mode),
confirmed_lines: uniqueStrings(pilot.evidence.confirmed_facts),
inference_lines: uniqueStrings(pilot.evidence.inferred_facts),
unknown_lines: uniqueStrings(pilot.evidence.unknown_facts),
limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]),
next_step_line: nextStepFor(mode, pilot),
internal_mechanics_allowed: false,
must_not_claim: buildMustNotClaim(pilot),
reason_codes: uniqueStrings(reasonCodes)
};
}

View File

@ -0,0 +1,82 @@
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
export interface AssistantMcpDiscoveryDebugAttachmentFields {
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
mcp_discovery_entry_status: string | null;
mcp_discovery_attempted: boolean;
mcp_discovery_hot_runtime_wired: false;
mcp_discovery_bridge_status: string | null;
mcp_discovery_answer_mode: string | null;
mcp_discovery_business_fact_answer_allowed: boolean;
mcp_discovery_user_facing_response_allowed: boolean;
mcp_discovery_requires_clarification: boolean;
}
export interface AttachAssistantMcpDiscoveryDebugInput {
entryPoint?: unknown;
addressRuntimeMeta?: Record<string, unknown> | null;
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
const record = toRecordObject(value);
return (
record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint"
);
}
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
return input.entryPoint;
}
const runtimeMetaEntryPoint =
input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
}
export function buildAssistantMcpDiscoveryDebugAttachmentFields(
input: AttachAssistantMcpDiscoveryDebugInput
): AssistantMcpDiscoveryDebugAttachmentFields {
const entryPoint = resolveEntryPoint(input);
const bridge = toRecordObject(entryPoint?.bridge);
const answerDraft = toRecordObject(bridge?.answer_draft);
return {
assistant_mcp_discovery_entry_point_v1: entryPoint,
mcp_discovery_entry_status: toNonEmptyString(entryPoint?.entry_status),
mcp_discovery_attempted: Boolean(entryPoint?.discovery_attempted),
mcp_discovery_hot_runtime_wired: false,
mcp_discovery_bridge_status: toNonEmptyString(bridge?.bridge_status),
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
mcp_discovery_requires_clarification: bridge?.requires_user_clarification === true
};
}
export function attachAssistantMcpDiscoveryDebug<T extends Record<string, unknown>>(
debugPayload: T,
input: AttachAssistantMcpDiscoveryDebugInput
): T & AssistantMcpDiscoveryDebugAttachmentFields {
return {
...debugPayload,
...buildAssistantMcpDiscoveryDebugAttachmentFields(input)
};
}

View File

@ -0,0 +1,363 @@
import {
executeAddressMcpQuery,
type AddressMcpMetadataRowsResult
} from "./addressMcpClient";
import {
buildAssistantMcpDiscoveryRuntimeDryRun,
type AssistantMcpDiscoveryRuntimeDryRunContract,
type AssistantMcpDiscoveryRuntimeStepContract
} from "./assistantMcpDiscoveryRuntimeAdapter";
import type { AssistantMcpDiscoveryPlannerContract } from "./assistantMcpDiscoveryPlanner";
import {
resolveAssistantMcpDiscoveryEvidence,
type AssistantMcpDiscoveryEvidenceContract,
type AssistantMcpDiscoveryProbeResult
} from "./assistantMcpDiscoveryPolicy";
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
import type { AddressFilterSet } from "../types/addressQuery";
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
"assistant_mcp_discovery_pilot_executor_v1" as const;
export type AssistantMcpDiscoveryPilotStatus =
| "executed"
| "skipped_needs_clarification"
| "blocked"
| "unsupported";
export interface AssistantMcpDiscoveryPilotExecutorDeps {
executeAddressMcpQuery: typeof executeAddressMcpQuery;
}
export interface AssistantMcpDiscoveryPilotExecutionContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryPilotExecutor";
pilot_status: AssistantMcpDiscoveryPilotStatus;
pilot_scope: "counterparty_lifecycle_query_documents_v1";
dry_run: AssistantMcpDiscoveryRuntimeDryRunContract;
mcp_execution_performed: boolean;
executed_primitives: string[];
skipped_primitives: string[];
probe_results: AssistantMcpDiscoveryProbeResult[];
evidence: AssistantMcpDiscoveryEvidenceContract;
source_rows_summary: string | null;
query_limitations: string[];
reason_codes: string[];
}
type AddressMcpQueryExecutorResult = Awaited<ReturnType<typeof executeAddressMcpQuery>>;
const DEFAULT_DEPS: AssistantMcpDiscoveryPilotExecutorDeps = {
executeAddressMcpQuery
};
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target: string[], value: string): void {
const text = value.trim();
if (text && !target.includes(text)) {
target.push(text);
}
}
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "period_from" | "period_to"> {
if (!dateScope) {
return {};
}
const yearMatch = dateScope.match(/^(\d{4})$/);
if (yearMatch) {
return {
period_from: `${yearMatch[1]}-01-01`,
period_to: `${yearMatch[1]}-12-31`
};
}
const dateMatch = dateScope.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (dateMatch) {
const date = `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
return {
period_from: date,
period_to: date
};
}
return {};
}
function buildLifecycleFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref;
const counterparty = firstEntityCandidate(planner);
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
const dateScope = toNonEmptyString(meaning?.explicit_date_scope);
return {
...dateScopeToFilters(dateScope),
...(counterparty ? { counterparty } : {}),
...(organization ? { organization } : {}),
limit: planner.discovery_plan.execution_budget.max_rows_per_probe,
sort: "period_asc"
};
}
function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const meaning = planner.discovery_plan.turn_meaning_ref;
const domain = String(meaning?.asked_domain_family ?? "").toLowerCase();
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const combined = `${domain} ${action}`;
return (
planner.proposed_primitives.includes("query_documents") &&
(combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age"))
);
}
function skippedProbeResult(step: AssistantMcpDiscoveryRuntimeStepContract, limitation: string): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: step.primitive_id,
status: "skipped",
rows_received: 0,
rows_matched: 0,
limitation
};
}
function queryResultToProbeResult(
primitiveId: string,
result: AddressMcpQueryExecutorResult
): AssistantMcpDiscoveryProbeResult {
return {
primitive_id: primitiveId,
status: result.error ? "error" : "ok",
rows_received: result.fetched_rows,
rows_matched: result.matched_rows,
limitation: result.error
};
}
function summarizeRows(result: AddressMcpQueryExecutorResult): string | null {
if (result.error) {
return null;
}
if (result.fetched_rows <= 0) {
return "0 MCP document rows fetched";
}
return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`;
}
function buildConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] {
if (result.error || result.matched_rows <= 0) {
return [];
}
return [
counterparty
? `1C activity rows were found for counterparty ${counterparty}`
: "1C activity rows were found for the requested counterparty scope"
];
}
function buildInferredFacts(result: AddressMcpQueryExecutorResult): string[] {
if (result.error || result.fetched_rows <= 0) {
return [];
}
return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"];
}
function buildUnknownFacts(): string[] {
return ["Legal registration date is not proven by this MCP discovery pilot"];
}
function buildEmptyEvidence(
planner: AssistantMcpDiscoveryPlannerContract,
dryRun: AssistantMcpDiscoveryRuntimeDryRunContract,
probeResults: AssistantMcpDiscoveryProbeResult[],
reason: string
): AssistantMcpDiscoveryEvidenceContract {
return resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
unknownFacts: [reason],
queryLimitations: [reason],
recommendedNextProbe: dryRun.user_facing_fallback
});
}
export async function executeAssistantMcpDiscoveryPilot(
planner: AssistantMcpDiscoveryPlannerContract,
deps: AssistantMcpDiscoveryPilotExecutorDeps = DEFAULT_DEPS
): Promise<AssistantMcpDiscoveryPilotExecutionContract> {
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
const reasonCodes = [...dryRun.reason_codes];
const executedPrimitives: string[] = [];
const skippedPrimitives: string[] = [];
const probeResults: AssistantMcpDiscoveryProbeResult[] = [];
const queryLimitations: string[] = [];
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");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "blocked",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["MCP discovery pilot was blocked before execution"],
reason_codes: reasonCodes
};
}
if (dryRun.adapter_status !== "dry_run_ready") {
pushReason(reasonCodes, "pilot_needs_clarification_before_mcp_execution");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot needs more scope before execution");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "skipped_needs_clarification",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["MCP discovery pilot needs more scope before execution"],
reason_codes: reasonCodes
};
}
if (!isLifecyclePilotEligible(planner)) {
pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution");
for (const step of dryRun.execution_steps) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_scope_unsupported_for_live_execution"));
}
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot scope is not implemented yet");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["MCP discovery pilot scope is not implemented yet"],
reason_codes: reasonCodes
};
}
let queryResult: AddressMcpQueryExecutorResult | null = null;
const counterparty = firstEntityCandidate(planner);
const filters = buildLifecycleFilters(planner);
const selection = selectAddressRecipe("counterparty_activity_lifecycle", filters);
if (!selection.selected_recipe) {
pushReason(reasonCodes, "pilot_lifecycle_recipe_not_available");
const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Lifecycle recipe is not available");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "unsupported",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: false,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: null,
query_limitations: ["Lifecycle recipe is not available"],
reason_codes: reasonCodes
};
}
const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters);
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "query_documents") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents"));
continue;
}
queryResult = await deps.executeAddressMcpQuery({
query: recipePlan.query,
limit: recipePlan.limit,
account_scope: recipePlan.account_scope
});
executedPrimitives.push(step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_query_documents_mcp_error");
} else {
pushReason(reasonCodes, "pilot_query_documents_mcp_executed");
}
}
const sourceRowsSummary = queryResult ? summarizeRows(queryResult) : null;
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: queryResult ? buildConfirmedFacts(queryResult, counterparty) : [],
inferredFacts: queryResult ? buildInferredFacts(queryResult) : [],
unknownFacts: buildUnknownFacts(),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: "counterparty_lifecycle_query_documents_v1",
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
export type AssistantMcpDiscoveryPilotMetadataResult = AddressMcpMetadataRowsResult;

View File

@ -0,0 +1,215 @@
import {
buildAssistantMcpDiscoveryPlan,
type AssistantMcpDiscoveryPlanContract,
type AssistantMcpDiscoveryPrimitive,
type AssistantMcpDiscoveryTurnMeaningRef
} from "./assistantMcpDiscoveryPolicy";
import {
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
type AssistantMcpCatalogPlanReview
} from "./assistantMcpCatalogIndex";
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
export type AssistantMcpDiscoveryPlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
export interface AssistantMcpDiscoveryPlannerInput {
semanticDataNeed?: string | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
}
export interface AssistantMcpDiscoveryPlannerContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryPlanner";
planner_status: AssistantMcpDiscoveryPlannerStatus;
semantic_data_need: string | null;
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
required_axes: string[];
discovery_plan: AssistantMcpDiscoveryPlanContract;
catalog_review: AssistantMcpCatalogPlanReview;
reason_codes: string[];
}
interface PlannerRecipe {
semanticDataNeed: string;
primitives: AssistantMcpDiscoveryPrimitive[];
axes: string[];
reason: string;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function lower(value: unknown): string {
return String(value ?? "").trim().toLowerCase();
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target: string[], value: string): void {
const text = value.trim();
if (text && !target.includes(text)) {
target.push(text);
}
}
function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
if (hasEntity(meaning)) {
pushUnique(axes, "counterparty");
}
if (toNonEmptyString(meaning?.explicit_organization_scope)) {
pushUnique(axes, "organization");
}
if (toNonEmptyString(meaning?.explicit_date_scope)) {
pushUnique(axes, "period");
}
}
function includesAny(text: string, tokens: string[]): boolean {
return tokens.some((token) => text.includes(token));
}
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const meaning = input.turnMeaning ?? null;
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes: string[] = [];
addScopeAxes(axes, meaning);
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "counterparty value-flow evidence",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: "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");
pushUnique(axes, "evidence_basis");
return {
semanticDataNeed: "counterparty lifecycle evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
};
}
if (includesAny(combined, ["metadata", "schema", "catalog"])) {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
};
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
return {
semanticDataNeed: "entity discovery evidence",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
};
}
return {
semanticDataNeed: "unclassified 1C discovery need",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
};
}
function statusFrom(
plan: AssistantMcpDiscoveryPlanContract,
review: AssistantMcpCatalogPlanReview
): AssistantMcpDiscoveryPlannerStatus {
if (plan.plan_status === "blocked" || review.review_status === "catalog_blocked") {
return "blocked";
}
if (plan.plan_status !== "allowed" || review.review_status !== "catalog_compatible") {
return "needs_clarification";
}
return "ready_for_execution";
}
export function planAssistantMcpDiscovery(
input: AssistantMcpDiscoveryPlannerInput
): AssistantMcpDiscoveryPlannerContract {
const recipe = recipeFor(input);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const reasonCodes: string[] = [];
pushReason(reasonCodes, recipe.reason);
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed,
turnMeaning: input.turnMeaning,
proposedPrimitives: recipe.primitives,
requiredAxes: recipe.axes
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
const plannerStatus = statusFrom(plan, review);
if (plannerStatus === "ready_for_execution") {
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
} else if (plannerStatus === "blocked") {
pushReason(reasonCodes, "planner_blocked_by_policy_or_catalog");
} else {
pushReason(reasonCodes, "planner_needs_more_user_or_scope_context");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: review,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,194 @@
import {
getAssistantMcpCatalogPrimitive,
type AssistantMcpCatalogEvidenceFloor
} from "./assistantMcpCatalogIndex";
import {
type AssistantMcpDiscoveryPlannerContract,
type AssistantMcpDiscoveryPlannerStatus
} from "./assistantMcpDiscoveryPlanner";
import type { AssistantMcpDiscoveryPrimitive } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION =
"assistant_mcp_discovery_runtime_dry_run_v1" as const;
export type AssistantMcpDiscoveryRuntimeAdapterStatus =
| "dry_run_ready"
| "needs_clarification"
| "blocked";
export type AssistantMcpDiscoveryRuntimeStepStatus = "ready" | "missing_axes" | "blocked";
export interface AssistantMcpDiscoveryRuntimeStepContract {
sequence: number;
primitive_id: AssistantMcpDiscoveryPrimitive;
step_status: AssistantMcpDiscoveryRuntimeStepStatus;
purpose: string;
provided_axes: string[];
required_axis_options: string[][];
missing_axis_options: string[][];
optional_axes: string[];
expected_fact_kinds: string[];
evidence_floor: AssistantMcpCatalogEvidenceFloor;
runtime_must_execute: true;
dry_run_only: true;
stop_condition: string;
}
export interface AssistantMcpDiscoveryEvidenceGateRequirement {
required: true;
expected_inputs: string[];
answer_may_use_raw_model_claims: false;
}
export interface AssistantMcpDiscoveryRuntimeDryRunContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeAdapter";
adapter_status: AssistantMcpDiscoveryRuntimeAdapterStatus;
planner_status: AssistantMcpDiscoveryPlannerStatus;
mcp_execution_performed: false;
execution_steps: AssistantMcpDiscoveryRuntimeStepContract[];
execution_budget: {
max_probe_count: number;
max_rows_per_probe: number;
};
evidence_gate: AssistantMcpDiscoveryEvidenceGateRequirement;
user_facing_fallback: string | null;
reason_codes: string[];
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
for (const value of values) {
const text = value.trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function stepStatusFor(input: {
adapterStatus: AssistantMcpDiscoveryRuntimeAdapterStatus;
missingAxisOptions: string[][];
}): AssistantMcpDiscoveryRuntimeStepStatus {
if (input.adapterStatus === "blocked") {
return "blocked";
}
if (input.missingAxisOptions.length > 0) {
return "missing_axes";
}
return "ready";
}
function stopConditionFor(evidenceFloor: AssistantMcpCatalogEvidenceFloor): string {
if (evidenceFloor === "rows_matched") {
return "stop_after_allowed_probe_returns_matched_rows_or_reports_no_match";
}
if (evidenceFloor === "rows_received") {
return "stop_after_allowed_probe_returns_rows_or_reports_empty_source";
}
if (evidenceFloor === "source_summary") {
return "stop_after_allowed_probe_returns_source_summary_or_limitation";
}
return "stop_without_fact_claim";
}
function adapterStatusFor(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryRuntimeAdapterStatus {
if (planner.planner_status === "blocked" || planner.catalog_review.review_status === "catalog_blocked") {
return "blocked";
}
if (planner.planner_status !== "ready_for_execution" || planner.catalog_review.review_status !== "catalog_compatible") {
return "needs_clarification";
}
return "dry_run_ready";
}
function fallbackFor(status: AssistantMcpDiscoveryRuntimeAdapterStatus): string | null {
if (status === "dry_run_ready") {
return null;
}
if (status === "blocked") {
return "explain_that_runtime_policy_blocked_mcp_discovery";
}
return "ask_for_missing_scope_before_mcp_discovery";
}
export function buildAssistantMcpDiscoveryRuntimeDryRun(
planner: AssistantMcpDiscoveryPlannerContract
): AssistantMcpDiscoveryRuntimeDryRunContract {
const adapterStatus = adapterStatusFor(planner);
const reasonCodes = uniqueStrings([
...planner.reason_codes,
...planner.discovery_plan.reason_codes,
...planner.catalog_review.reason_codes
]);
const providedAxes = uniqueStrings(planner.required_axes);
const executionSteps = planner.discovery_plan.allowed_primitives.map((primitiveId, index) => {
const catalog = getAssistantMcpCatalogPrimitive(primitiveId);
const missingAxisOptions = planner.catalog_review.missing_axes_by_primitive[primitiveId] ?? [];
return {
sequence: index + 1,
primitive_id: primitiveId,
step_status: stepStatusFor({ adapterStatus, missingAxisOptions }),
purpose: catalog.purpose,
provided_axes: providedAxes,
required_axis_options: catalog.required_axes_any_of,
missing_axis_options: missingAxisOptions,
optional_axes: catalog.optional_axes,
expected_fact_kinds: catalog.output_fact_kinds,
evidence_floor: catalog.evidence_floor,
runtime_must_execute: true,
dry_run_only: true,
stop_condition: stopConditionFor(catalog.evidence_floor)
} satisfies AssistantMcpDiscoveryRuntimeStepContract;
});
if (adapterStatus === "dry_run_ready") {
pushReason(reasonCodes, "runtime_dry_run_ready_without_mcp_execution");
} else if (adapterStatus === "blocked") {
pushReason(reasonCodes, "runtime_dry_run_blocked_before_mcp_execution");
} else {
pushReason(reasonCodes, "runtime_dry_run_needs_clarification_before_mcp_execution");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeAdapter",
adapter_status: adapterStatus,
planner_status: planner.planner_status,
mcp_execution_performed: false,
execution_steps: executionSteps,
execution_budget: planner.discovery_plan.execution_budget,
evidence_gate: {
required: true,
expected_inputs: [
"probe_results",
"confirmed_facts",
"inferred_facts",
"unknown_facts",
"source_rows_summary",
"query_limitations"
],
answer_may_use_raw_model_claims: false
},
user_facing_fallback: fallbackFor(adapterStatus),
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,124 @@
import {
buildAssistantMcpDiscoveryAnswerDraft,
type AssistantMcpDiscoveryAnswerDraftContract
} from "./assistantMcpDiscoveryAnswerAdapter";
import {
executeAssistantMcpDiscoveryPilot,
type AssistantMcpDiscoveryPilotExecutionContract,
type AssistantMcpDiscoveryPilotExecutorDeps
} from "./assistantMcpDiscoveryPilotExecutor";
import {
planAssistantMcpDiscovery,
type AssistantMcpDiscoveryPlannerContract
} from "./assistantMcpDiscoveryPlanner";
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
"assistant_mcp_discovery_runtime_bridge_v1" as const;
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
| "answer_draft_ready"
| "checked_sources_only"
| "needs_clarification"
| "blocked"
| "unsupported";
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
semanticDataNeed?: string | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
}
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
bridge_status: AssistantMcpDiscoveryRuntimeBridgeStatus;
hot_runtime_wired: false;
planner: AssistantMcpDiscoveryPlannerContract;
pilot: AssistantMcpDiscoveryPilotExecutionContract;
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
user_facing_response_allowed: boolean;
business_fact_answer_allowed: boolean;
requires_user_clarification: boolean;
reason_codes: string[];
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function bridgeStatusFor(
pilot: AssistantMcpDiscoveryPilotExecutionContract,
draft: AssistantMcpDiscoveryAnswerDraftContract
): AssistantMcpDiscoveryRuntimeBridgeStatus {
if (draft.answer_mode === "blocked" || pilot.pilot_status === "blocked") {
return "blocked";
}
if (draft.answer_mode === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.pilot_status === "unsupported") {
return "unsupported";
}
if (draft.answer_mode === "checked_sources_only") {
return "checked_sources_only";
}
return "answer_draft_ready";
}
function businessFactAnswerAllowed(draft: AssistantMcpDiscoveryAnswerDraftContract): boolean {
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
}
export async function runAssistantMcpDiscoveryRuntimeBridge(
input: AssistantMcpDiscoveryRuntimeBridgeInput
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
const planner = planAssistantMcpDiscovery({
semanticDataNeed: input.semanticDataNeed,
turnMeaning: input.turnMeaning
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
bridge_status: bridgeStatus,
hot_runtime_wired: false,
planner,
pilot,
answer_draft: answerDraft,
user_facing_response_allowed: bridgeStatus !== "blocked",
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
requires_user_clarification: bridgeStatus === "needs_clarification",
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,121 @@
import {
runAssistantMcpDiscoveryRuntimeBridge,
type AssistantMcpDiscoveryRuntimeBridgeContract
} from "./assistantMcpDiscoveryRuntimeBridge";
import {
buildAssistantMcpDiscoveryTurnInput,
type AssistantMcpDiscoveryTurnInputContract,
type BuildAssistantMcpDiscoveryTurnInputAdapterInput
} from "./assistantMcpDiscoveryTurnInputAdapter";
import type { AssistantMcpDiscoveryPilotExecutorDeps } from "./assistantMcpDiscoveryPilotExecutor";
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION =
"assistant_mcp_discovery_runtime_entry_point_v1" as const;
export type AssistantMcpDiscoveryRuntimeEntryPointStatus =
| "bridge_executed"
| "skipped_not_applicable"
| "skipped_needs_more_context";
export interface RunAssistantMcpDiscoveryRuntimeEntryPointInput
extends BuildAssistantMcpDiscoveryTurnInputAdapterInput {
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
}
export interface AssistantMcpDiscoveryRuntimeEntryPointContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint";
entry_status: AssistantMcpDiscoveryRuntimeEntryPointStatus;
hot_runtime_wired: false;
discovery_attempted: boolean;
turn_input: AssistantMcpDiscoveryTurnInputContract;
bridge: AssistantMcpDiscoveryRuntimeBridgeContract | null;
reason_codes: string[];
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function skippedContract(input: {
status: Exclude<AssistantMcpDiscoveryRuntimeEntryPointStatus, "bridge_executed">;
turnInput: AssistantMcpDiscoveryTurnInputContract;
reason: string;
}): AssistantMcpDiscoveryRuntimeEntryPointContract {
const reasonCodes = uniqueStrings(input.turnInput.reason_codes);
pushReason(reasonCodes, input.reason);
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: input.status,
hot_runtime_wired: false,
discovery_attempted: false,
turn_input: input.turnInput,
bridge: null,
reason_codes: reasonCodes
};
}
export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
input: RunAssistantMcpDiscoveryRuntimeEntryPointInput
): Promise<AssistantMcpDiscoveryRuntimeEntryPointContract> {
const turnInput = buildAssistantMcpDiscoveryTurnInput(input);
if (!turnInput.should_run_discovery) {
return skippedContract({
status: "skipped_not_applicable",
turnInput,
reason: "runtime_entry_point_skipped_supported_exact_turn"
});
}
if (!turnInput.turn_meaning_ref) {
return skippedContract({
status: "skipped_needs_more_context",
turnInput,
reason: "runtime_entry_point_skipped_missing_discovery_turn_meaning"
});
}
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
semanticDataNeed: turnInput.semantic_data_need,
turnMeaning: turnInput.turn_meaning_ref,
deps: input.deps
});
const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]);
pushReason(reasonCodes, "runtime_entry_point_bridge_executed");
pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer");
return {
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: turnInput,
bridge,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,278 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
"assistant_mcp_discovery_turn_input_v1" as const;
export type AssistantMcpDiscoveryTurnInputStatus = "ready" | "needs_more_context" | "not_applicable";
export type AssistantMcpDiscoveryTurnInputSource =
| "assistant_turn_meaning"
| "predecompose_contract"
| "raw_text"
| "none";
export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
assistantTurnMeaning?: Record<string, unknown> | null;
predecomposeContract?: Record<string, unknown> | null;
userMessage?: string | null;
effectiveMessage?: string | null;
}
export interface AssistantMcpDiscoveryTurnInputContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryTurnInputAdapter";
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
should_run_discovery: boolean;
semantic_data_need: string | null;
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
source_signal: AssistantMcpDiscoveryTurnInputSource;
reason_codes: string[];
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target: string[], value: unknown): void {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function compactLower(value: unknown): string {
return String(value ?? "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
}
function candidateValue(value: unknown): string | null {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (
toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text)
);
}
function collectEntityCandidates(value: unknown): string[] {
const result: string[] = [];
if (Array.isArray(value)) {
for (const item of value) {
pushUnique(result, candidateValue(item));
}
return result;
}
pushUnique(result, candidateValue(value));
return result;
}
function collectPredecomposeEntities(predecompose: Record<string, unknown> | null): {
counterparty: string | null;
organization: string | null;
} {
const entities = toRecordObject(predecompose?.entities);
return {
counterparty: toNonEmptyString(entities?.counterparty),
organization: toNonEmptyString(entities?.organization)
};
}
function collectDateScope(predecompose: Record<string, unknown> | null): string | null {
const period = toRecordObject(predecompose?.period);
const asOfDate = toNonEmptyString(period?.as_of_date);
const periodFrom = toNonEmptyString(period?.period_from);
const periodTo = toNonEmptyString(period?.period_to);
if (asOfDate) {
return asOfDate;
}
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
return yearFrom[1];
}
if (periodFrom && periodTo) {
return `${periodFrom}..${periodTo}`;
}
return periodFrom ?? periodTo ?? null;
}
function hasLifecycleSignal(text: string): boolean {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(
text
);
}
function semanticNeedFor(input: {
domain: string | null;
action: string | null;
unsupported: string | null;
lifecycleSignal: boolean;
}): string | null {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
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: {
unsupported: string | null;
lifecycleSignal: boolean;
semanticDataNeed: string | null;
explicitIntentCandidate: string | null;
}): boolean {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
return false;
}
export function buildAssistantMcpDiscoveryTurnInput(
input: BuildAssistantMcpDiscoveryTurnInputAdapterInput
): AssistantMcpDiscoveryTurnInputContract {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const semanticDataNeed = semanticNeedFor({
domain: rawDomain,
action: rawAction,
unsupported,
lifecycleSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
};
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
}
if (toNonEmptyString(turnMeaning.asked_action_family)) {
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
}
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
}
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
}
if (turnMeaning.stale_replay_forbidden) {
cleanTurnMeaning.stale_replay_forbidden = true;
}
const runDiscovery = shouldRunDiscovery({
unsupported,
lifecycleSignal,
semanticDataNeed,
explicitIntentCandidate
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning"
: predecomposeContract
? "predecompose_contract"
: lifecycleSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
if (!runDiscovery) {
pushReason(reasonCodes, "mcp_discovery_not_applicable_for_supported_exact_turn");
}
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes
};
}

View File

@ -86,6 +86,7 @@ export interface AssistantAddressRuntimeMetaForDeep {
predecomposeContract?: Record<string, unknown> | null;
semanticExtractionContract?: Record<string, unknown> | null;
orchestrationContract?: Record<string, unknown> | null;
mcpDiscoveryRuntimeEntryPoint?: Record<string, unknown> | null;
}
export interface AssistantRequirement {

View File

@ -92,7 +92,72 @@ describe("assistant address lane response runtime adapter", () => {
capability_binding_response_guard: expect.objectContaining({
schema_version: "assistant_capability_binding_response_guard_v1",
applied: false
})
}),
assistant_mcp_discovery_entry_point_v1: null,
mcp_discovery_attempted: false,
mcp_discovery_hot_runtime_wired: false
})
);
});
it("attaches MCP discovery summary from predecompose runtime meta without changing the reply", () => {
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-mcp",
userMessage: "raw",
effectiveAddressUserMessage: "raw",
addressLane: {
handled: true,
reply_text: "answer",
reply_type: "partial_coverage",
debug: {}
},
llmPreDecomposeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { adapter_status: "ready" },
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: { answer_mode: "confirmed_with_bounded_inference" }
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: () => ({}),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: () => null,
appendItem: () => {},
getSession: () => ({ session_id: "asst-mcp", updated_at: "", items: [], investigation_state: null } as any),
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-mcp",
finalizeAddressTurn: () => ({
response: {
ok: true
}
})
});
expect(runtime.response).toEqual({ ok: true });
expect(runtime.debug).toEqual(
expect.objectContaining({
mcp_discovery_entry_status: "bridge_executed",
mcp_discovery_attempted: true,
mcp_discovery_bridge_status: "answer_draft_ready",
mcp_discovery_answer_mode: "confirmed_with_bounded_inference",
mcp_discovery_business_fact_answer_allowed: true,
mcp_discovery_hot_runtime_wired: false
})
);
});

View File

@ -154,6 +154,9 @@ describe("assistant debug payload assembler", () => {
binding_action: "observe_only"
})
);
expect(payload.assistant_mcp_discovery_entry_point_v1).toBeNull();
expect(payload.mcp_discovery_attempted).toBe(false);
expect(payload.mcp_discovery_hot_runtime_wired).toBe(false);
});
it("omits optional fields when they are not provided", () => {
@ -188,4 +191,35 @@ describe("assistant debug payload assembler", () => {
expect(payload.answer_contract_stage4_v1?.legacy_blocks_present).toContain("Что сломано");
expect(payload.answer_contract_stage4_v1?.legacy_blocks_present).toContain("Ограничения");
});
it("attaches MCP discovery entry point summary when runtime meta provides it", () => {
const input = baseInput();
input.addressRuntimeMetaForDeep = {
...input.addressRuntimeMetaForDeep,
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { adapter_status: "ready" },
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: { answer_mode: "confirmed_with_bounded_inference" }
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
};
const payload = buildDeepAnalysisDebugPayload(input);
expect(payload.mcp_discovery_entry_status).toBe("bridge_executed");
expect(payload.mcp_discovery_attempted).toBe(true);
expect(payload.mcp_discovery_bridge_status).toBe("answer_draft_ready");
expect(payload.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
expect(payload.mcp_discovery_business_fact_answer_allowed).toBe(true);
});
});

View File

@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";
import { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } from "../src/services/assistantMcpDiscoveryPolicy";
import {
buildAssistantMcpCatalogIndex,
getAssistantMcpCatalogPrimitive,
reviewAssistantMcpDiscoveryPlanAgainstCatalog
} from "../src/services/assistantMcpCatalogIndex";
describe("assistant MCP catalog index", () => {
it("declares a catalog contract for every reviewed discovery primitive", () => {
const index = buildAssistantMcpCatalogIndex();
const primitiveIds = index.primitives.map((entry) => entry.primitive_id);
expect(index.reason_codes).toContain("catalog_covers_all_discovery_primitives");
expect(primitiveIds).toEqual([...ASSISTANT_MCP_DISCOVERY_PRIMITIVES]);
for (const entry of index.primitives) {
expect(entry.safe_for_model_planning).toBe(true);
expect(entry.runtime_must_execute).toBe(true);
expect(entry.required_axes_any_of.length).toBeGreaterThan(0);
expect(entry.output_fact_kinds.length).toBeGreaterThan(0);
}
});
it("marks a counterparty turnover discovery plan as catalog-compatible when required axes exist", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "counterparty turnover evidence",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
},
proposedPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis"],
requiredAxes: ["counterparty", "period", "aggregate_axis", "amount"]
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
expect(review.review_status).toBe("catalog_compatible");
expect(review.reason_codes).toContain("catalog_plan_compatible");
expect(review.missing_axes_by_primitive).toEqual({});
expect(review.evidence_floors.query_movements).toBe("rows_matched");
});
it("asks for more axes before document discovery can run safely", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "document evidence",
turnMeaning: {
asked_domain_family: "counterparty_documents",
asked_action_family: "list_documents"
},
proposedPrimitives: ["query_documents"],
requiredAxes: ["amount"]
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
expect(review.review_status).toBe("needs_more_axes");
expect(review.reason_codes).toContain("catalog_required_axes_missing_for_primitive");
expect(review.missing_axes_by_primitive.query_documents).toEqual([
["document"],
["counterparty"],
["contract"],
["period", "organization"]
]);
});
it("preserves source-summary evidence floors for metadata and coverage primitives", () => {
expect(getAssistantMcpCatalogPrimitive("inspect_1c_metadata").evidence_floor).toBe("source_summary");
expect(getAssistantMcpCatalogPrimitive("probe_coverage").evidence_floor).toBe("source_summary");
expect(getAssistantMcpCatalogPrimitive("resolve_entity_reference").evidence_floor).toBe("rows_matched");
});
it("turns a non-allowed discovery plan into a catalog-level blocked review", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "raw model sql",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
},
proposedPrimitives: ["raw_sql"],
requiredAxes: ["counterparty"]
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
expect(plan.plan_status).toBe("blocked");
expect(review.review_status).toBe("catalog_blocked");
expect(review.reason_codes).toContain("catalog_review_received_non_allowed_plan");
expect(review.reason_codes).toContain("catalog_plan_blocked");
});
});

View File

@ -0,0 +1,109 @@
import { describe, expect, it, vi } from "vitest";
import { buildAssistantMcpDiscoveryAnswerDraft } from "../src/services/assistantMcpDiscoveryAnswerAdapter";
import { executeAssistantMcpDiscoveryPilot } from "../src/services/assistantMcpDiscoveryPilotExecutor";
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpQuery: vi.fn(async () => ({
fetched_rows: rows.length,
matched_rows: error ? 0 : rows.length,
raw_rows: 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({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.internal_mechanics_allowed).toBe(false);
expect(draft.headline).toContain("подтвержденная активность");
expect(draft.confirmed_lines[0]).toContain("SVK");
expect(draft.inference_lines[0]).toContain("may be inferred");
expect(draft.unknown_lines).toContain("Legal registration date is not proven by this MCP discovery pilot");
expect(draft.must_not_claim).toContain("Do not present inferred activity duration as a formally confirmed legal fact.");
expect(draft.reason_codes).toContain("answer_contains_unknown_fact_boundary");
});
it("uses checked-sources mode when MCP failed and avoids confirmed facts", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([], "MCP fetch failed: timeout"));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("checked_sources_only");
expect(draft.confirmed_lines).toEqual([]);
expect(draft.limitation_lines).toContain("MCP fetch failed: timeout");
expect(draft.next_step_line).toContain("MCP");
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
});
it("asks for clarification when discovery did not execute due to missing scope", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toBe("Нужно уточнить контекст перед поиском в 1С.");
expect(draft.next_step_line).toContain("Уточните контрагента");
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("does not leak primitive names or query text into user-facing lines", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const userFacing = [
draft.headline,
...draft.confirmed_lines,
...draft.inference_lines,
...draft.unknown_lines,
...draft.limitation_lines,
draft.next_step_line ?? ""
].join("\n");
expect(userFacing).not.toContain("query_documents");
expect(userFacing).not.toContain("SELECT");
expect(userFacing).not.toContain("ВЫБРАТЬ");
expect(userFacing).not.toContain("primitive");
});
});

View File

@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { attachAssistantMcpDiscoveryDebug } from "../src/services/assistantMcpDiscoveryDebugAttachment";
function entryPointContract(overrides: Record<string, unknown> = {}) {
return {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { adapter_status: "ready" },
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
},
reason_codes: ["runtime_entry_point_bridge_executed"],
...overrides
};
}
describe("assistant MCP discovery debug attachment", () => {
it("attaches a validated entry point contract and exposes summary flags", () => {
const debug = attachAssistantMcpDiscoveryDebug(
{ trace_id: "trace-1" },
{ addressRuntimeMeta: { mcpDiscoveryRuntimeEntryPoint: entryPointContract() } }
);
expect(debug.assistant_mcp_discovery_entry_point_v1?.schema_version).toBe(
"assistant_mcp_discovery_runtime_entry_point_v1"
);
expect(debug.mcp_discovery_entry_status).toBe("bridge_executed");
expect(debug.mcp_discovery_attempted).toBe(true);
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
expect(debug.mcp_discovery_bridge_status).toBe("answer_draft_ready");
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
expect(debug.mcp_discovery_requires_clarification).toBe(false);
});
it("keeps safe null flags when no validated entry point exists", () => {
const debug = attachAssistantMcpDiscoveryDebug(
{ trace_id: "trace-1" },
{ addressRuntimeMeta: { mcpDiscoveryRuntimeEntryPoint: { schema_version: "wrong" } } }
);
expect(debug.assistant_mcp_discovery_entry_point_v1).toBeNull();
expect(debug.mcp_discovery_entry_status).toBeNull();
expect(debug.mcp_discovery_attempted).toBe(false);
expect(debug.mcp_discovery_hot_runtime_wired).toBe(false);
expect(debug.mcp_discovery_bridge_status).toBeNull();
expect(debug.mcp_discovery_answer_mode).toBeNull();
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);
expect(debug.mcp_discovery_requires_clarification).toBe(false);
});
});

View File

@ -0,0 +1,106 @@
import { describe, expect, it, vi } from "vitest";
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
import { executeAssistantMcpDiscoveryPilot } from "../src/services/assistantMcpDiscoveryPilotExecutor";
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpQuery: vi.fn(async () => ({
fetched_rows: rows.length,
matched_rows: error ? 0 : rows.length,
raw_rows: 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({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
const deps = buildDeps([
{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" },
{ Период: "2023-12-20T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY", Контрагент: "SVK" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["query_documents"]);
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "probe_coverage", "explain_evidence_basis"]);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.evidence.confirmed_facts[0]).toContain("SVK");
expect(result.evidence.inferred_facts[0]).toContain("may be inferred");
expect(result.evidence.unknown_facts).toContain("Legal registration date is not proven by this MCP discovery pilot");
expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched lifecycle scope");
expect(result.reason_codes).toContain("pilot_query_documents_mcp_executed");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
expect(String(call?.query ?? "")).toContain("Документ.ПоступлениеНаРасчетныйСчет");
expect(call?.limit).toBeGreaterThan(0);
});
it("does not execute MCP when dry-run still needs clarification", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
}
});
const deps = buildDeps([]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("skipped_needs_clarification");
expect(result.mcp_execution_performed).toBe(false);
expect(result.evidence.evidence_status).toBe("insufficient");
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
});
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"]
}
});
const deps = buildDeps([]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("unsupported");
expect(result.mcp_execution_performed).toBe(false);
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
});
it("records MCP errors as limitations without converting them into facts", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
const deps = buildDeps([], "MCP fetch failed: timeout");
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.mcp_execution_performed).toBe(true);
expect(result.evidence.evidence_status).toBe("insufficient");
expect(result.evidence.confirmed_facts).toEqual([]);
expect(result.query_limitations).toContain("MCP fetch failed: timeout");
expect(result.reason_codes).toContain("pilot_query_documents_mcp_error");
});
});

View File

@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest";
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
describe("assistant MCP discovery planner", () => {
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
});
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["period", "counterparty"]);
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("builds a document discovery plan without falling back to movement primitives", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("query_movements");
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_documents",
"probe_coverage",
"explain_evidence_basis"
]);
expect(result.required_axes).toEqual(["counterparty", "document_date", "coverage_target", "evidence_basis"]);
});
it("uses metadata-only planning when the user asks about available schema surface", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.required_axes).toEqual(["metadata_scope"]);
expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary");
});
it("does not mark an unclassified turn as executable without turn meaning context", () => {
const result = planAssistantMcpDiscovery({});
expect(result.planner_status).toBe("needs_clarification");
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
});

View File

@ -0,0 +1,97 @@
import { describe, expect, it } from "vitest";
import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner";
import { buildAssistantMcpDiscoveryRuntimeDryRun } from "../src/services/assistantMcpDiscoveryRuntimeAdapter";
describe("assistant MCP discovery runtime adapter", () => {
it("turns a catalog-compatible value-flow plan into an execution-ready dry-run package", () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
expect(dryRun.adapter_status).toBe("dry_run_ready");
expect(dryRun.mcp_execution_performed).toBe(false);
expect(dryRun.user_facing_fallback).toBeNull();
expect(dryRun.execution_steps.map((step) => step.primitive_id)).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(dryRun.execution_steps.every((step) => step.step_status === "ready")).toBe(true);
expect(dryRun.execution_steps.every((step) => step.dry_run_only)).toBe(true);
expect(dryRun.evidence_gate).toEqual({
required: true,
expected_inputs: [
"probe_results",
"confirmed_facts",
"inferred_facts",
"unknown_facts",
"source_rows_summary",
"query_limitations"
],
answer_may_use_raw_model_claims: false
});
expect(dryRun.reason_codes).toContain("runtime_dry_run_ready_without_mcp_execution");
});
it("keeps execution in clarification state when catalog axes are missing", () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
}
});
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
const movementStep = dryRun.execution_steps.find((step) => step.primitive_id === "query_movements");
expect(dryRun.adapter_status).toBe("needs_clarification");
expect(dryRun.mcp_execution_performed).toBe(false);
expect(dryRun.user_facing_fallback).toBe("ask_for_missing_scope_before_mcp_discovery");
expect(movementStep?.step_status).toBe("missing_axes");
expect(movementStep?.missing_axis_options).toContainEqual(["period", "counterparty"]);
expect(dryRun.reason_codes).toContain("runtime_dry_run_needs_clarification_before_mcp_execution");
});
it("keeps blocked planner output blocked before any MCP execution", () => {
const planner = planAssistantMcpDiscovery({});
planner.planner_status = "blocked";
planner.discovery_plan.plan_status = "blocked";
planner.catalog_review.review_status = "catalog_blocked";
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
expect(dryRun.adapter_status).toBe("blocked");
expect(dryRun.mcp_execution_performed).toBe(false);
expect(dryRun.user_facing_fallback).toBe("explain_that_runtime_policy_blocked_mcp_discovery");
expect(dryRun.execution_steps.every((step) => step.step_status === "blocked")).toBe(true);
expect(dryRun.reason_codes).toContain("runtime_dry_run_blocked_before_mcp_execution");
});
it("uses source-summary stop conditions for metadata-only dry runs", () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog"
}
});
const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner);
expect(dryRun.adapter_status).toBe("dry_run_ready");
expect(dryRun.execution_steps).toHaveLength(1);
expect(dryRun.execution_steps[0]).toMatchObject({
primitive_id: "inspect_1c_metadata",
evidence_floor: "source_summary",
stop_condition: "stop_after_allowed_probe_returns_source_summary_or_limitation"
});
});
});

View File

@ -0,0 +1,93 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantMcpDiscoveryRuntimeBridge } from "../src/services/assistantMcpDiscoveryRuntimeBridge";
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpQuery: vi.fn(async () => ({
fetched_rows: rows.length,
matched_rows: error ? 0 : rows.length,
raw_rows: rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery runtime bridge", () => {
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
});
expect(result.schema_version).toBe("assistant_mcp_discovery_runtime_bridge_v1");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.hot_runtime_wired).toBe(false);
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.user_facing_response_allowed).toBe(true);
expect(result.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer");
});
it("keeps missing scope as clarification and does not authorize a business fact answer", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.business_fact_answer_allowed).toBe(false);
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
});
it("keeps unsupported ready plans outside the hot answer path", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "document",
asked_action_family: "documents",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("unsupported");
expect(result.hot_runtime_wired).toBe(false);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.business_fact_answer_allowed).toBe(false);
expect(result.reason_codes).toContain("runtime_bridge_status_unsupported");
});
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["SVK"]
},
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
});
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(userFacing).not.toContain("query_documents");
expect(userFacing).not.toContain("runtime_bridge");
expect(userFacing).not.toContain("primitive");
});
});

View File

@ -0,0 +1,76 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantMcpDiscoveryRuntimeEntryPoint } from "../src/services/assistantMcpDiscoveryRuntimeEntryPoint";
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpQuery: vi.fn(async () => ({
fetched_rows: rows.length,
matched_rows: error ? 0 : rows.length,
raw_rows: 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({
userMessage: "Сколько лет мы работаем с Группа СВК?",
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: {}
},
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.hot_runtime_wired).toBe(false);
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.semantic_data_need).toBe("counterparty lifecycle evidence");
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(result.reason_codes).toContain("runtime_entry_point_bridge_executed");
});
it("skips supported exact turns before any discovery execution", async () => {
const deps = buildDeps([]);
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "list_documents",
explicit_intent_candidate: "list_documents_by_counterparty",
explicit_entity_candidates: [{ value: "SVK" }]
},
deps
});
expect(result.entry_status).toBe("skipped_not_applicable");
expect(result.discovery_attempted).toBe(false);
expect(result.bridge).toBeNull();
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
expect(result.reason_codes).toContain("runtime_entry_point_skipped_supported_exact_turn");
});
it("passes unsupported-but-understood value turns into bridge with normalized entity scope", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
explicit_entity_candidates: [{ value: "SVK" }]
},
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildDeps([])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.bridge?.bridge_status).toBe("unsupported");
expect(result.bridge?.hot_runtime_wired).toBe(false);
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
});
});

View File

@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { buildAssistantMcpDiscoveryTurnInput } from "../src/services/assistantMcpDiscoveryTurnInputAdapter";
describe("assistant MCP discovery turn input adapter", () => {
it("maps unsupported assistant turn meaning into a discovery-ready value-flow input", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true,
explicit_entity_candidates: [{ type: "counterparty", value: "SVK", source: "current_turn_loose_entity_tail" }]
},
predecomposeContract: {
entities: { counterparty: "Группа СВК", organization: "Альтернатива" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.turn_meaning_ref?.stale_replay_forbidden).toBe(true);
});
it("bootstraps lifecycle discovery from raw user wording and predecompose scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "Сколько лет мы работаем с Группа СВК?",
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: { period_from: null, period_to: null, as_of_date: null }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.source_signal).toBe("predecompose_contract");
expect(result.semantic_data_need).toBe("counterparty lifecycle evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "counterparty_lifecycle",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_lifecycle_signal_detected");
});
it("does not activate discovery for supported exact current-turn intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "list_documents",
explicit_intent_candidate: "list_documents_by_counterparty",
explicit_entity_candidates: [{ value: "SVK" }],
stale_replay_forbidden: false
}
});
expect(result.adapter_status).toBe("not_applicable");
expect(result.should_run_discovery).toBe(false);
expect(result.turn_meaning_ref).toBeNull();
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("never serializes object candidates as [object Object]", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
explicit_entity_candidates: [{ type: "counterparty", value: "SVK" }]
}
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK"]);
expect(result.turn_meaning_ref?.explicit_entity_candidates).not.toContain("[object Object]");
});
});