Planner Autonomy: вынести MCP chain templates в route fabric

This commit is contained in:
dctouch 2026-05-01 11:37:50 +03:00
parent b12f370784
commit 5cd4d459fe
6 changed files with 593 additions and 238 deletions

View File

@ -6,6 +6,7 @@ exports.searchAssistantMcpCatalogPrimitivesByFactAxis = searchAssistantMcpCatalo
exports.searchAssistantMcpCatalogPrimitivesByMetadataSurface = searchAssistantMcpCatalogPrimitivesByMetadataSurface;
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
exports.getAssistantMcpCatalogChainTemplate = getAssistantMcpCatalogChainTemplate;
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1";
@ -183,6 +184,117 @@ const PRIMITIVE_CONTRACTS = [
}
];
const PRIMITIVE_CONTRACT_MAP = new Map(PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract]));
const CHAIN_TEMPLATES = [
{
chain_id: "metadata_inspection",
semantic_data_need: "1C metadata evidence",
chain_summary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
fallback_primitives: ["inspect_1c_metadata"],
base_required_axes: ["metadata_scope"],
supported_fact_families: ["schema_surface"],
supported_action_families: ["inspect_catalog", "inspect_documents", "inspect_registers", "inspect_fields", "inspect_surface"],
planning_tags: ["metadata", "surface_inspection"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "catalog_drilldown",
semantic_data_need: "catalog drilldown metadata evidence",
chain_summary: "Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
fallback_primitives: ["inspect_1c_metadata"],
base_required_axes: ["metadata_scope"],
supported_fact_families: ["schema_surface"],
supported_action_families: ["inspect_catalog", "inspect_surface"],
planning_tags: ["metadata", "surface_inspection", "drilldown"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "entity_resolution",
semantic_data_need: "entity discovery evidence",
chain_summary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
fallback_primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
base_required_axes: ["business_entity", "coverage_target"],
supported_fact_families: ["entity_grounding"],
supported_action_families: ["search_business_entity"],
planning_tags: ["subject_resolution", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "document_evidence",
semantic_data_need: "document evidence",
chain_summary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
fallback_primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
base_required_axes: ["coverage_target"],
supported_fact_families: ["document_evidence", "activity_lifecycle"],
supported_action_families: ["list_documents", "activity_duration"],
planning_tags: ["document", "subject_resolution", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "movement_evidence",
semantic_data_need: "movement evidence",
chain_summary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
fallback_primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
base_required_axes: ["coverage_target"],
supported_fact_families: ["movement_evidence", "value_flow"],
supported_action_families: ["list_movements", "turnover", "payout", "net_value_flow"],
planning_tags: ["movement", "subject_resolution", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "value_flow",
semantic_data_need: "counterparty value-flow evidence",
chain_summary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
fallback_primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
base_required_axes: ["aggregate_axis", "amount", "coverage_target"],
supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout", "net_value_flow"],
planning_tags: ["movement", "aggregation", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "value_flow_comparison",
semantic_data_need: "bidirectional value-flow comparison evidence",
chain_summary: "Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
fallback_primitives: ["query_movements", "probe_coverage"],
base_required_axes: ["amount", "coverage_target"],
supported_fact_families: ["value_flow"],
supported_action_families: ["net_value_flow"],
planning_tags: ["movement", "comparison", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "value_flow_ranking",
semantic_data_need: "ranked value-flow evidence",
chain_summary: "Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
fallback_primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
base_required_axes: ["aggregate_axis", "amount", "coverage_target"],
supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout"],
planning_tags: ["movement", "ranking", "aggregation", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "lifecycle",
semantic_data_need: "counterparty lifecycle evidence",
chain_summary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
fallback_primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
base_required_axes: ["document_date", "coverage_target", "evidence_basis"],
supported_fact_families: ["activity_lifecycle"],
supported_action_families: ["activity_duration"],
planning_tags: ["document", "explanation", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
}
];
const CHAIN_TEMPLATE_MAP = new Map(CHAIN_TEMPLATES.map((template) => [template.chain_id, template]));
function toStringSet(values) {
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
}
@ -516,10 +628,18 @@ function buildAssistantMcpCatalogIndex() {
else {
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
}
const unknownChainPrimitives = CHAIN_TEMPLATES.flatMap((template) => template.fallback_primitives.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive)));
if (unknownChainPrimitives.length > 0) {
pushReason(reasonCodes, "catalog_chain_template_references_unknown_primitive");
}
else {
pushReason(reasonCodes, "catalog_chain_templates_reference_reviewed_primitives");
}
return {
schema_version: exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex",
primitives: PRIMITIVE_CONTRACTS,
chain_templates: CHAIN_TEMPLATES,
reason_codes: reasonCodes
};
}
@ -530,6 +650,13 @@ function getAssistantMcpCatalogPrimitive(primitive) {
}
return contract;
}
function getAssistantMcpCatalogChainTemplate(chainId) {
const template = CHAIN_TEMPLATE_MAP.get(chainId);
if (!template) {
throw new Error(`Missing MCP catalog chain template: ${chainId}`);
}
return template;
}
function reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan) {
const reasonCodes = [];
const axisSet = toStringSet(plan.required_axes);

View File

@ -35,6 +35,28 @@ function pushUnique(target, value) {
target.push(text);
}
}
function pushAllUnique(target, values) {
for (const value of values) {
pushUnique(target, value);
}
}
function recipeFromCatalogChainTemplate(input) {
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)(input.chainId);
const axes = [...input.axes];
pushAllUnique(axes, template.base_required_axes);
return {
semanticDataNeed: input.semanticDataNeed ?? template.semantic_data_need,
chainId: template.chain_id,
chainSummary: input.chainSummary ?? template.chain_summary,
primitives: input.primitives ?? template.fallback_primitives,
axes,
reason: input.reason,
extraReasons: [
`planner_instantiated_catalog_chain_template_${template.chain_id}`,
...(input.extraReasons ?? [])
]
};
}
function hasEntity(meaning) {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
@ -306,60 +328,60 @@ function recipeFor(input) {
const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input);
if (thinSurfaceRouteFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
chainSummary: "Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref",
chainSummary: "Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (thinSurfaceRouteFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
chainSummary: "Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref",
chainSummary: "Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (thinSurfaceRouteFamily === "catalog_drilldown") {
pushUnique(axes, "metadata_scope");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("catalog_drilldown");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "catalog drilldown metadata evidence",
return recipeFromCatalogChainTemplate({
chainId: "catalog_drilldown",
chainSummary: "Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref",
chainSummary: "Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "value_flow") {
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
@ -368,47 +390,45 @@ function recipeFor(input) {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("value_flow_comparison");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: false
});
return {
semanticDataNeed: "bidirectional value-flow comparison evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow_comparison",
chainSummary: "Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("value_flow_ranking");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "ranked value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow_ranking",
chainSummary: "Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization");
@ -425,17 +445,17 @@ function recipeFor(input) {
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "organization-scoped value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow",
chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
semanticDataNeed: "organization-scoped value-flow evidence",
chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
@ -443,70 +463,68 @@ function recipeFor(input) {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("value_flow");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "counterparty value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("lifecycle");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "counterparty lifecycle evidence",
return recipeFromCatalogChainTemplate({
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_lifecycle_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("metadata_inspection");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "1C metadata evidence",
return recipeFromCatalogChainTemplate({
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "movement_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
@ -514,38 +532,38 @@ function recipeFor(input) {
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
chainSummary: "Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
chainSummary: "Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
semanticDataNeed: template.semantic_data_need,
extraReasons: primitiveSelection.reasonCodes
};
});
}
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "document_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_documents", "probe_coverage"],
@ -553,55 +571,53 @@ function recipeFor(input) {
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
chainSummary: "Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
chainSummary: "Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
semanticDataNeed: template.semantic_data_need,
extraReasons: primitiveSelection.reasonCodes
};
});
}
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("entity_resolution");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "entity discovery evidence",
return recipeFromCatalogChainTemplate({
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
@ -621,83 +637,64 @@ function recipeFor(input) {
if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "counterparty value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_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",
return recipeFromCatalogChainTemplate({
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
};
});
}
if (includesAny(combined, ["metadata", "schema", "catalog"])) {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
return recipeFromCatalogChainTemplate({
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
};
});
}
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_recipe"
};
});
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
});
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "entity discovery evidence",
return recipeFromCatalogChainTemplate({
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
};
});
}
return {
semanticDataNeed: "unclassified 1C discovery need",
return recipeFromCatalogChainTemplate({
chainId: "metadata_inspection",
semanticDataNeed: "unclassified 1C discovery need",
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
};
});
}
function statusFrom(plan, review) {
if (plan.plan_status === "blocked" || review.review_status === "catalog_blocked") {

View File

@ -9,6 +9,16 @@ export const ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = "assistant_mcp_c
export type AssistantMcpCatalogEvidenceFloor = "none" | "rows_received" | "rows_matched" | "source_summary";
export type AssistantMcpCatalogPlanReviewStatus = "catalog_compatible" | "needs_more_axes" | "catalog_blocked";
export type AssistantMcpCatalogChainTemplateId =
| "metadata_inspection"
| "catalog_drilldown"
| "value_flow"
| "value_flow_comparison"
| "value_flow_ranking"
| "lifecycle"
| "movement_evidence"
| "document_evidence"
| "entity_resolution";
export interface AssistantMcpCatalogPrimitiveContract {
primitive_id: AssistantMcpDiscoveryPrimitive;
@ -29,9 +39,23 @@ export interface AssistantMcpCatalogIndexContract {
schema_version: typeof ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION;
policy_owner: "assistantMcpCatalogIndex";
primitives: AssistantMcpCatalogPrimitiveContract[];
chain_templates: AssistantMcpCatalogChainTemplateContract[];
reason_codes: string[];
}
export interface AssistantMcpCatalogChainTemplateContract {
chain_id: AssistantMcpCatalogChainTemplateId;
semantic_data_need: string;
chain_summary: string;
fallback_primitives: AssistantMcpDiscoveryPrimitive[];
base_required_axes: string[];
supported_fact_families: string[];
supported_action_families: string[];
planning_tags: string[];
safe_for_model_planning: true;
requires_evidence_gate: true;
}
export interface AssistantMcpCatalogPlanReview {
schema_version: typeof ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION;
policy_owner: "assistantMcpCatalogIndex";
@ -220,6 +244,127 @@ const PRIMITIVE_CONTRACT_MAP = new Map<AssistantMcpDiscoveryPrimitive, Assistant
PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract])
);
const CHAIN_TEMPLATES: AssistantMcpCatalogChainTemplateContract[] = [
{
chain_id: "metadata_inspection",
semantic_data_need: "1C metadata evidence",
chain_summary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
fallback_primitives: ["inspect_1c_metadata"],
base_required_axes: ["metadata_scope"],
supported_fact_families: ["schema_surface"],
supported_action_families: ["inspect_catalog", "inspect_documents", "inspect_registers", "inspect_fields", "inspect_surface"],
planning_tags: ["metadata", "surface_inspection"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "catalog_drilldown",
semantic_data_need: "catalog drilldown metadata evidence",
chain_summary:
"Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
fallback_primitives: ["inspect_1c_metadata"],
base_required_axes: ["metadata_scope"],
supported_fact_families: ["schema_surface"],
supported_action_families: ["inspect_catalog", "inspect_surface"],
planning_tags: ["metadata", "surface_inspection", "drilldown"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "entity_resolution",
semantic_data_need: "entity discovery evidence",
chain_summary:
"Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
fallback_primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
base_required_axes: ["business_entity", "coverage_target"],
supported_fact_families: ["entity_grounding"],
supported_action_families: ["search_business_entity"],
planning_tags: ["subject_resolution", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "document_evidence",
semantic_data_need: "document evidence",
chain_summary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
fallback_primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
base_required_axes: ["coverage_target"],
supported_fact_families: ["document_evidence", "activity_lifecycle"],
supported_action_families: ["list_documents", "activity_duration"],
planning_tags: ["document", "subject_resolution", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "movement_evidence",
semantic_data_need: "movement evidence",
chain_summary:
"Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
fallback_primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
base_required_axes: ["coverage_target"],
supported_fact_families: ["movement_evidence", "value_flow"],
supported_action_families: ["list_movements", "turnover", "payout", "net_value_flow"],
planning_tags: ["movement", "subject_resolution", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "value_flow",
semantic_data_need: "counterparty value-flow evidence",
chain_summary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
fallback_primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
base_required_axes: ["aggregate_axis", "amount", "coverage_target"],
supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout", "net_value_flow"],
planning_tags: ["movement", "aggregation", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "value_flow_comparison",
semantic_data_need: "bidirectional value-flow comparison evidence",
chain_summary:
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
fallback_primitives: ["query_movements", "probe_coverage"],
base_required_axes: ["amount", "coverage_target"],
supported_fact_families: ["value_flow"],
supported_action_families: ["net_value_flow"],
planning_tags: ["movement", "comparison", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "value_flow_ranking",
semantic_data_need: "ranked value-flow evidence",
chain_summary:
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
fallback_primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
base_required_axes: ["aggregate_axis", "amount", "coverage_target"],
supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout"],
planning_tags: ["movement", "ranking", "aggregation", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
},
{
chain_id: "lifecycle",
semantic_data_need: "counterparty lifecycle evidence",
chain_summary:
"Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
fallback_primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
base_required_axes: ["document_date", "coverage_target", "evidence_basis"],
supported_fact_families: ["activity_lifecycle"],
supported_action_families: ["activity_duration"],
planning_tags: ["document", "explanation", "coverage"],
safe_for_model_planning: true,
requires_evidence_gate: true
}
];
const CHAIN_TEMPLATE_MAP = new Map<AssistantMcpCatalogChainTemplateId, AssistantMcpCatalogChainTemplateContract>(
CHAIN_TEMPLATES.map((template) => [template.chain_id, template])
);
function toStringSet(values: string[]): Set<string> {
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
}
@ -626,10 +771,19 @@ export function buildAssistantMcpCatalogIndex(): AssistantMcpCatalogIndexContrac
} else {
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
}
const unknownChainPrimitives = CHAIN_TEMPLATES.flatMap((template) =>
template.fallback_primitives.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive))
);
if (unknownChainPrimitives.length > 0) {
pushReason(reasonCodes, "catalog_chain_template_references_unknown_primitive");
} else {
pushReason(reasonCodes, "catalog_chain_templates_reference_reviewed_primitives");
}
return {
schema_version: ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex",
primitives: PRIMITIVE_CONTRACTS,
chain_templates: CHAIN_TEMPLATES,
reason_codes: reasonCodes
};
}
@ -644,6 +798,16 @@ export function getAssistantMcpCatalogPrimitive(
return contract;
}
export function getAssistantMcpCatalogChainTemplate(
chainId: AssistantMcpCatalogChainTemplateId
): AssistantMcpCatalogChainTemplateContract {
const template = CHAIN_TEMPLATE_MAP.get(chainId);
if (!template) {
throw new Error(`Missing MCP catalog chain template: ${chainId}`);
}
return template;
}
export function reviewAssistantMcpDiscoveryPlanAgainstCatalog(
plan: AssistantMcpDiscoveryPlanContract
): AssistantMcpCatalogPlanReview {

View File

@ -5,10 +5,12 @@ import {
type AssistantMcpDiscoveryTurnMeaningRef
} from "./assistantMcpDiscoveryPolicy";
import {
getAssistantMcpCatalogChainTemplate,
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
searchAssistantMcpCatalogPrimitivesByFactAxis,
searchAssistantMcpCatalogPrimitivesByMetadataSurface,
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
type AssistantMcpCatalogChainTemplateId,
type AssistantMcpCatalogPlanReview
} from "./assistantMcpCatalogIndex";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
@ -119,6 +121,38 @@ function pushUnique(target: string[], value: string): void {
}
}
function pushAllUnique(target: string[], values: string[]): void {
for (const value of values) {
pushUnique(target, value);
}
}
function recipeFromCatalogChainTemplate(input: {
chainId: AssistantMcpCatalogChainTemplateId;
axes: string[];
primitives?: AssistantMcpDiscoveryPrimitive[];
reason: string;
extraReasons?: string[];
chainSummary?: string;
semanticDataNeed?: string;
}): PlannerRecipe {
const template = getAssistantMcpCatalogChainTemplate(input.chainId);
const axes = [...input.axes];
pushAllUnique(axes, template.base_required_axes);
return {
semanticDataNeed: input.semanticDataNeed ?? template.semantic_data_need,
chainId: template.chain_id,
chainSummary: input.chainSummary ?? template.chain_summary,
primitives: input.primitives ?? template.fallback_primitives,
axes,
reason: input.reason,
extraReasons: [
`planner_instantiated_catalog_chain_template_${template.chain_id}`,
...(input.extraReasons ?? [])
]
};
}
function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
@ -460,63 +494,63 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input);
if (thinSurfaceRouteFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref",
chainSummary:
"Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (thinSurfaceRouteFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref",
chainSummary:
"Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (thinSurfaceRouteFamily === "catalog_drilldown") {
pushUnique(axes, "metadata_scope");
const template = getAssistantMcpCatalogChainTemplate("catalog_drilldown");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "catalog drilldown metadata evidence",
return recipeFromCatalogChainTemplate({
chainId: "catalog_drilldown",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref",
chainSummary:
"Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "value_flow") {
@ -526,50 +560,46 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const template = getAssistantMcpCatalogChainTemplate("value_flow_comparison");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: false
});
return {
semanticDataNeed: "bidirectional value-flow comparison evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow_comparison",
chainSummary:
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("value_flow_ranking");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "ranked value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow_ranking",
chainSummary:
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason:
dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization");
@ -586,19 +616,19 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "organization-scoped value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow",
axes,
primitives: primitiveSelection.primitives,
semanticDataNeed: "organization-scoped value-flow evidence",
chainSummary:
"Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: primitiveSelection.primitives,
axes,
reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
@ -606,74 +636,72 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const template = getAssistantMcpCatalogChainTemplate("value_flow");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "counterparty value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis");
const template = getAssistantMcpCatalogChainTemplate("lifecycle");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "counterparty lifecycle evidence",
return recipeFromCatalogChainTemplate({
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_lifecycle_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope");
const template = getAssistantMcpCatalogChainTemplate("metadata_inspection");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "1C metadata evidence",
return recipeFromCatalogChainTemplate({
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "movement_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
@ -681,40 +709,40 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
chainSummary:
"Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
semanticDataNeed: template.semantic_data_need,
extraReasons: primitiveSelection.reasonCodes
};
});
}
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "document_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_documents", "probe_coverage"],
@ -722,58 +750,56 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
chainSummary:
"Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
semanticDataNeed: template.semantic_data_need,
extraReasons: primitiveSelection.reasonCodes
};
});
}
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("entity_resolution");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "entity discovery evidence",
return recipeFromCatalogChainTemplate({
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: primitiveSelection.primitives,
axes,
primitives: primitiveSelection.primitives,
reason:
graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe",
extraReasons: primitiveSelection.reasonCodes
};
});
}
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
@ -795,89 +821,70 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "counterparty value-flow evidence",
return recipeFromCatalogChainTemplate({
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_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",
return recipeFromCatalogChainTemplate({
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
};
});
}
if (includesAny(combined, ["metadata", "schema", "catalog"])) {
pushUnique(axes, "metadata_scope");
return {
semanticDataNeed: "1C metadata evidence",
return recipeFromCatalogChainTemplate({
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
};
});
}
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "movement evidence",
return recipeFromCatalogChainTemplate({
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_recipe"
};
});
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
return recipeFromCatalogChainTemplate({
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
});
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "entity discovery evidence",
return recipeFromCatalogChainTemplate({
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
};
});
}
return {
semanticDataNeed: "unclassified 1C discovery need",
return recipeFromCatalogChainTemplate({
chainId: "metadata_inspection",
semanticDataNeed: "unclassified 1C discovery need",
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
};
});
}
function statusFrom(

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } from "../src/services/assistantMcpDiscoveryPolicy";
import {
buildAssistantMcpCatalogIndex,
getAssistantMcpCatalogChainTemplate,
getAssistantMcpCatalogPrimitive,
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
@ -15,6 +16,7 @@ describe("assistant MCP catalog index", () => {
const primitiveIds = index.primitives.map((entry) => entry.primitive_id);
expect(index.reason_codes).toContain("catalog_covers_all_discovery_primitives");
expect(index.reason_codes).toContain("catalog_chain_templates_reference_reviewed_primitives");
expect(primitiveIds).toEqual([...ASSISTANT_MCP_DISCOVERY_PRIMITIVES]);
for (const entry of index.primitives) {
expect(entry.safe_for_model_planning).toBe(true);
@ -26,6 +28,53 @@ describe("assistant MCP catalog index", () => {
expect(entry.required_axes_any_of.length).toBeGreaterThan(0);
expect(entry.output_fact_kinds.length).toBeGreaterThan(0);
}
expect(index.chain_templates.map((entry) => entry.chain_id)).toEqual([
"metadata_inspection",
"catalog_drilldown",
"entity_resolution",
"document_evidence",
"movement_evidence",
"value_flow",
"value_flow_comparison",
"value_flow_ranking",
"lifecycle"
]);
for (const template of index.chain_templates) {
expect(template.safe_for_model_planning).toBe(true);
expect(template.requires_evidence_gate).toBe(true);
expect(template.semantic_data_need.length).toBeGreaterThan(0);
expect(template.chain_summary.length).toBeGreaterThan(0);
expect(template.fallback_primitives.length).toBeGreaterThan(0);
for (const primitive of template.fallback_primitives) {
expect(primitiveIds).toContain(primitive);
}
}
});
it("exposes reusable chain templates for planner route-fabric selection", () => {
const documentTemplate = getAssistantMcpCatalogChainTemplate("document_evidence");
const movementTemplate = getAssistantMcpCatalogChainTemplate("movement_evidence");
const valueFlowTemplate = getAssistantMcpCatalogChainTemplate("value_flow");
const valueFlowComparisonTemplate = getAssistantMcpCatalogChainTemplate("value_flow_comparison");
const valueFlowRankingTemplate = getAssistantMcpCatalogChainTemplate("value_flow_ranking");
expect(documentTemplate.fallback_primitives).toEqual([
"resolve_entity_reference",
"query_documents",
"probe_coverage"
]);
expect(movementTemplate.fallback_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"probe_coverage"
]);
expect(valueFlowTemplate.base_required_axes).toEqual(["aggregate_axis", "amount", "coverage_target"]);
expect(valueFlowComparisonTemplate.fallback_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(valueFlowRankingTemplate.fallback_primitives).toEqual([
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
});
it("can search reviewed primitives from data-need decomposition candidates", () => {

View File

@ -144,6 +144,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.selected_chain_id).toBe("document_evidence");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_fact_axis_search");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_document_evidence");
expect(result.reason_codes).not.toContain("planner_fell_back_to_recipe_primitives_after_empty_catalog_search");
});
@ -253,6 +254,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_movement_from_data_need_graph");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_decomposition_candidates");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_movement_evidence");
});
it("filters conflicting document-vs-movement primitives when confirmed metadata surface recommends movements", () => {
@ -412,6 +414,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("entity_resolution");
expect(result.reason_codes).toContain("planner_selected_entity_resolution_recipe");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_entity_resolution");
expect(result.reason_codes).not.toContain("planner_selected_document_from_confirmed_metadata_surface_ref");
expect(result.reason_codes).not.toContain("planner_selected_movement_from_confirmed_metadata_surface_ref");
});
@ -464,6 +467,7 @@ describe("assistant MCP discovery planner", () => {
"calendar_month"
]);
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow");
});
it("does not collapse a ranking-shaped value graph into entity-resolution just because no subject is preselected", () => {
@ -497,6 +501,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow_ranking");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
@ -532,6 +537,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow_ranking");
});
it("does not collapse incoming-vs-outgoing comparison into entity-resolution when no counterparty is preselected", () => {
@ -564,6 +570,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "amount", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow_comparison");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
@ -599,6 +606,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["organization", "period", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow_comparison");
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
@ -740,6 +748,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow");
});
it("keeps generic one-sided open totals in organization clarification instead of forcing entity resolution", () => {
const result = planAssistantMcpDiscovery({
@ -777,6 +786,7 @@ describe("assistant MCP discovery planner", () => {
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["organization"]);
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
@ -822,6 +832,7 @@ describe("assistant MCP discovery planner", () => {
]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow");
});
it("keeps metadata-scoped movement evidence in clarification instead of forcing entity resolution", () => {