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.searchAssistantMcpCatalogPrimitivesByMetadataSurface = searchAssistantMcpCatalogPrimitivesByMetadataSurface;
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex; exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive; exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
exports.getAssistantMcpCatalogChainTemplate = getAssistantMcpCatalogChainTemplate;
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog; exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy"); const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1"; 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 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) { function toStringSet(values) {
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0)); return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
} }
@ -516,10 +628,18 @@ function buildAssistantMcpCatalogIndex() {
else { else {
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives"); 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 { return {
schema_version: exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex", policy_owner: "assistantMcpCatalogIndex",
primitives: PRIMITIVE_CONTRACTS, primitives: PRIMITIVE_CONTRACTS,
chain_templates: CHAIN_TEMPLATES,
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
@ -530,6 +650,13 @@ function getAssistantMcpCatalogPrimitive(primitive) {
} }
return contract; 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) { function reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan) {
const reasonCodes = []; const reasonCodes = [];
const axisSet = toStringSet(plan.required_axes); const axisSet = toStringSet(plan.required_axes);

View File

@ -35,6 +35,28 @@ function pushUnique(target, value) {
target.push(text); 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) { function hasEntity(meaning) {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0; return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
} }
@ -306,60 +328,60 @@ function recipeFor(input) {
const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input); const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input);
if (thinSurfaceRouteFamily === "document_evidence") { if (thinSurfaceRouteFamily === "document_evidence") {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref", 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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (thinSurfaceRouteFamily === "movement_evidence") { if (thinSurfaceRouteFamily === "movement_evidence") {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref", 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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (thinSurfaceRouteFamily === "catalog_drilldown") { if (thinSurfaceRouteFamily === "catalog_drilldown") {
pushUnique(axes, "metadata_scope"); pushUnique(axes, "metadata_scope");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("catalog_drilldown");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "catalog drilldown metadata evidence",
chainId: "catalog_drilldown", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref", 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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "value_flow") { if (graphFactFamily === "value_flow") {
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) { if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
@ -368,47 +390,45 @@ function recipeFor(input) {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") { if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month"); pushUnique(axes, "calendar_month");
} }
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("value_flow_comparison");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action, actionFamily: action,
allowAggregateByAxis: false allowAggregateByAxis: false
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "bidirectional value-flow comparison evidence",
chainId: "value_flow_comparison", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph", reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) { if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis"); pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount"); pushUnique(axes, "amount");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("value_flow_ranking");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action, actionFamily: action,
allowAggregateByAxis: true allowAggregateByAxis: true
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "ranked value-flow evidence",
chainId: "value_flow_ranking", 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, axes,
primitives: primitiveSelection.primitives,
reason: dataNeedGraph.ranking_need === "bottom_asc" reason: dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph" ? "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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (openScopeTotalWithoutSubject) { if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization"); pushUnique(axes, "organization");
@ -425,17 +445,17 @@ function recipeFor(input) {
actionFamily: action, actionFamily: action,
allowAggregateByAxis: true allowAggregateByAxis: true
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow", 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, 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" reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph" ? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph", : "planner_selected_open_scope_value_flow_total_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
pushUnique(axes, "aggregate_axis"); pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount"); pushUnique(axes, "amount");
@ -443,70 +463,68 @@ function recipeFor(input) {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") { if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month"); pushUnique(axes, "calendar_month");
} }
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("value_flow");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action, actionFamily: action,
allowAggregateByAxis: true allowAggregateByAxis: true
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow", chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes, axes,
primitives: primitiveSelection.primitives,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month" reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph" ? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph", : "planner_selected_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "activity_lifecycle") { if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date"); pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis"); pushUnique(axes, "evidence_basis");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("lifecycle");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_lifecycle_from_data_need_graph", reason: "planner_selected_lifecycle_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "schema_surface") { if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope"); pushUnique(axes, "metadata_scope");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("metadata_inspection");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection", chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: primitiveSelection.primitives,
axes, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_from_data_need_graph", reason: "planner_selected_metadata_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "movement_evidence") { if (graphFactFamily === "movement_evidence") {
if (metadataScopedOpenLane) { if (metadataScopedOpenLane) {
pushUnique(axes, "organization"); pushUnique(axes, "organization");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"], fallbackPrimitives: ["query_movements", "probe_coverage"],
@ -514,38 +532,38 @@ function recipeFor(input) {
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph", 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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_data_need_graph", reason: "planner_selected_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "document_evidence") { if (graphFactFamily === "document_evidence") {
if (metadataScopedOpenLane) { if (metadataScopedOpenLane) {
pushUnique(axes, "organization"); pushUnique(axes, "organization");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_documents", "probe_coverage"], fallbackPrimitives: ["query_documents", "probe_coverage"],
@ -553,55 +571,53 @@ function recipeFor(input) {
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph", 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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_data_need_graph", reason: "planner_selected_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) { if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity"); pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = (0, assistantMcpCatalogIndex_1.getAssistantMcpCatalogChainTemplate)("entity_resolution");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution", 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, axes,
primitives: primitiveSelection.primitives,
reason: graphAction === "search_business_entity" reason: graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph" ? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe", : "planner_selected_entity_resolution_recipe",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) { if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice"); pushUnique(axes, "lane_family_choice");
@ -621,83 +637,64 @@ function recipeFor(input) {
if (requestedAggregationAxis === "month") { if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month"); pushUnique(axes, "calendar_month");
} }
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow", 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, axes,
reason: requestedAggregationAxis === "month" reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe" ? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_recipe" : "planner_selected_value_flow_recipe"
}; });
} }
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) { if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
pushUnique(axes, "document_date"); pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis"); pushUnique(axes, "evidence_basis");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle", 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, axes,
reason: "planner_selected_lifecycle_recipe" reason: "planner_selected_lifecycle_recipe"
}; });
} }
if (includesAny(combined, ["metadata", "schema", "catalog"])) { if (includesAny(combined, ["metadata", "schema", "catalog"])) {
pushUnique(axes, "metadata_scope"); pushUnique(axes, "metadata_scope");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection", 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, axes,
reason: "planner_selected_metadata_recipe" reason: "planner_selected_metadata_recipe"
}; });
} }
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) { if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", 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, axes,
reason: "planner_selected_movement_recipe" reason: "planner_selected_movement_recipe"
}; });
} }
if (includesAny(combined, ["document", "documents"])) { if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", 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, axes,
reason: "planner_selected_document_recipe" reason: "planner_selected_document_recipe"
}; });
} }
if (hasEntity(meaning)) { if (hasEntity(meaning)) {
pushUnique(axes, "business_entity"); pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution", 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, axes,
reason: "planner_selected_entity_resolution_recipe" reason: "planner_selected_entity_resolution_recipe"
}; });
} }
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "unclassified 1C discovery need",
chainId: "metadata_inspection", 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.", 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, axes,
reason: "planner_selected_clarification_recipe" reason: "planner_selected_clarification_recipe"
}; });
} }
function statusFrom(plan, review) { function statusFrom(plan, review) {
if (plan.plan_status === "blocked" || review.review_status === "catalog_blocked") { 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 AssistantMcpCatalogEvidenceFloor = "none" | "rows_received" | "rows_matched" | "source_summary";
export type AssistantMcpCatalogPlanReviewStatus = "catalog_compatible" | "needs_more_axes" | "catalog_blocked"; 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 { export interface AssistantMcpCatalogPrimitiveContract {
primitive_id: AssistantMcpDiscoveryPrimitive; primitive_id: AssistantMcpDiscoveryPrimitive;
@ -29,9 +39,23 @@ export interface AssistantMcpCatalogIndexContract {
schema_version: typeof ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION; schema_version: typeof ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION;
policy_owner: "assistantMcpCatalogIndex"; policy_owner: "assistantMcpCatalogIndex";
primitives: AssistantMcpCatalogPrimitiveContract[]; primitives: AssistantMcpCatalogPrimitiveContract[];
chain_templates: AssistantMcpCatalogChainTemplateContract[];
reason_codes: string[]; 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 { export interface AssistantMcpCatalogPlanReview {
schema_version: typeof ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION; schema_version: typeof ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION;
policy_owner: "assistantMcpCatalogIndex"; policy_owner: "assistantMcpCatalogIndex";
@ -220,6 +244,127 @@ const PRIMITIVE_CONTRACT_MAP = new Map<AssistantMcpDiscoveryPrimitive, Assistant
PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract]) 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> { function toStringSet(values: string[]): Set<string> {
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0)); return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
} }
@ -626,10 +771,19 @@ export function buildAssistantMcpCatalogIndex(): AssistantMcpCatalogIndexContrac
} else { } else {
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives"); 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 { return {
schema_version: ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION, schema_version: ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
policy_owner: "assistantMcpCatalogIndex", policy_owner: "assistantMcpCatalogIndex",
primitives: PRIMITIVE_CONTRACTS, primitives: PRIMITIVE_CONTRACTS,
chain_templates: CHAIN_TEMPLATES,
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
@ -644,6 +798,16 @@ export function getAssistantMcpCatalogPrimitive(
return contract; 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( export function reviewAssistantMcpDiscoveryPlanAgainstCatalog(
plan: AssistantMcpDiscoveryPlanContract plan: AssistantMcpDiscoveryPlanContract
): AssistantMcpCatalogPlanReview { ): AssistantMcpCatalogPlanReview {

View File

@ -5,10 +5,12 @@ import {
type AssistantMcpDiscoveryTurnMeaningRef type AssistantMcpDiscoveryTurnMeaningRef
} from "./assistantMcpDiscoveryPolicy"; } from "./assistantMcpDiscoveryPolicy";
import { import {
getAssistantMcpCatalogChainTemplate,
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates, searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
searchAssistantMcpCatalogPrimitivesByFactAxis, searchAssistantMcpCatalogPrimitivesByFactAxis,
searchAssistantMcpCatalogPrimitivesByMetadataSurface, searchAssistantMcpCatalogPrimitivesByMetadataSurface,
reviewAssistantMcpDiscoveryPlanAgainstCatalog, reviewAssistantMcpDiscoveryPlanAgainstCatalog,
type AssistantMcpCatalogChainTemplateId,
type AssistantMcpCatalogPlanReview type AssistantMcpCatalogPlanReview
} from "./assistantMcpCatalogIndex"; } from "./assistantMcpCatalogIndex";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph"; 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 { function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0; return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
} }
@ -460,63 +494,63 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input); const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input);
if (thinSurfaceRouteFamily === "document_evidence") { if (thinSurfaceRouteFamily === "document_evidence") {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", chainId: "document_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref",
chainSummary: chainSummary:
"Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.", "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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (thinSurfaceRouteFamily === "movement_evidence") { if (thinSurfaceRouteFamily === "movement_evidence") {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", chainId: "movement_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref",
chainSummary: chainSummary:
"Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.", "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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (thinSurfaceRouteFamily === "catalog_drilldown") { if (thinSurfaceRouteFamily === "catalog_drilldown") {
pushUnique(axes, "metadata_scope"); pushUnique(axes, "metadata_scope");
const template = getAssistantMcpCatalogChainTemplate("catalog_drilldown");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "catalog drilldown metadata evidence",
chainId: "catalog_drilldown", chainId: "catalog_drilldown",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref",
chainSummary: 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.", "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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "value_flow") { if (graphFactFamily === "value_flow") {
@ -526,50 +560,46 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") { if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month"); pushUnique(axes, "calendar_month");
} }
const template = getAssistantMcpCatalogChainTemplate("value_flow_comparison");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action, actionFamily: action,
allowAggregateByAxis: false allowAggregateByAxis: false
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "bidirectional value-flow comparison evidence",
chainId: "value_flow_comparison", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph", reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) { if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis"); pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount"); pushUnique(axes, "amount");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("value_flow_ranking");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action, actionFamily: action,
allowAggregateByAxis: true allowAggregateByAxis: true
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "ranked value-flow evidence",
chainId: "value_flow_ranking", 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, axes,
primitives: primitiveSelection.primitives,
reason: reason:
dataNeedGraph.ranking_need === "bottom_asc" dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph" ? "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 extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (openScopeTotalWithoutSubject) { if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization"); pushUnique(axes, "organization");
@ -586,19 +616,19 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
actionFamily: action, actionFamily: action,
allowAggregateByAxis: true allowAggregateByAxis: true
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow", chainId: "value_flow",
axes,
primitives: primitiveSelection.primitives,
semanticDataNeed: "organization-scoped value-flow evidence",
chainSummary: 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.", "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: reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month" requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph" ? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph", : "planner_selected_open_scope_value_flow_total_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
pushUnique(axes, "aggregate_axis"); pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount"); pushUnique(axes, "amount");
@ -606,74 +636,72 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") { if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month"); pushUnique(axes, "calendar_month");
} }
const template = getAssistantMcpCatalogChainTemplate("value_flow");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action, actionFamily: action,
allowAggregateByAxis: true allowAggregateByAxis: true
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow", chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes, axes,
primitives: primitiveSelection.primitives,
reason: reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month" requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph" ? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph", : "planner_selected_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "activity_lifecycle") { if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date"); pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis"); pushUnique(axes, "evidence_basis");
const template = getAssistantMcpCatalogChainTemplate("lifecycle");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_lifecycle_from_data_need_graph", reason: "planner_selected_lifecycle_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "schema_surface") { if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope"); pushUnique(axes, "metadata_scope");
const template = getAssistantMcpCatalogChainTemplate("metadata_inspection");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection", chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: primitiveSelection.primitives,
axes, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_from_data_need_graph", reason: "planner_selected_metadata_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "movement_evidence") { if (graphFactFamily === "movement_evidence") {
if (metadataScopedOpenLane) { if (metadataScopedOpenLane) {
pushUnique(axes, "organization"); pushUnique(axes, "organization");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"], fallbackPrimitives: ["query_movements", "probe_coverage"],
@ -681,40 +709,40 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", chainId: "movement_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
chainSummary: 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.", "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, semanticDataNeed: template.semantic_data_need,
axes,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("movement_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_movement_from_data_need_graph", reason: "planner_selected_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "document_evidence") { if (graphFactFamily === "document_evidence") {
if (metadataScopedOpenLane) { if (metadataScopedOpenLane) {
pushUnique(axes, "organization"); pushUnique(axes, "organization");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["query_documents", "probe_coverage"], fallbackPrimitives: ["query_documents", "probe_coverage"],
@ -722,58 +750,56 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", chainId: "document_evidence",
axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
chainSummary: 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.", "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, semanticDataNeed: template.semantic_data_need,
axes,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("document_evidence");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", 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, axes,
primitives: primitiveSelection.primitives,
reason: "planner_selected_document_from_data_need_graph", reason: "planner_selected_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) { if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity"); pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
const template = getAssistantMcpCatalogChainTemplate("entity_resolution");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({ const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph, dataNeedGraph,
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"], fallbackPrimitives: template.fallback_primitives,
requiredAxes: axes, requiredAxes: axes,
metadataSurface: input.metadataSurface, metadataSurface: input.metadataSurface,
actionFamily: action actionFamily: action
}); });
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution", 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, axes,
primitives: primitiveSelection.primitives,
reason: reason:
graphAction === "search_business_entity" graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph" ? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe", : "planner_selected_entity_resolution_recipe",
extraReasons: primitiveSelection.reasonCodes extraReasons: primitiveSelection.reasonCodes
}; });
} }
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) { if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
@ -795,89 +821,70 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
if (requestedAggregationAxis === "month") { if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month"); pushUnique(axes, "calendar_month");
} }
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow", 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, axes,
reason: requestedAggregationAxis === "month" reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe" ? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_recipe" : "planner_selected_value_flow_recipe"
}; });
} }
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) { if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
pushUnique(axes, "document_date"); pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis"); pushUnique(axes, "evidence_basis");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle", 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, axes,
reason: "planner_selected_lifecycle_recipe" reason: "planner_selected_lifecycle_recipe"
}; });
} }
if (includesAny(combined, ["metadata", "schema", "catalog"])) { if (includesAny(combined, ["metadata", "schema", "catalog"])) {
pushUnique(axes, "metadata_scope"); pushUnique(axes, "metadata_scope");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection", 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, axes,
reason: "planner_selected_metadata_recipe" reason: "planner_selected_metadata_recipe"
}; });
} }
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) { if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "movement evidence",
chainId: "movement_evidence", 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, axes,
reason: "planner_selected_movement_recipe" reason: "planner_selected_movement_recipe"
}; });
} }
if (includesAny(combined, ["document", "documents"])) { if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "document evidence",
chainId: "document_evidence", 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, axes,
reason: "planner_selected_document_recipe" reason: "planner_selected_document_recipe"
}; });
} }
if (hasEntity(meaning)) { if (hasEntity(meaning)) {
pushUnique(axes, "business_entity"); pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target"); pushUnique(axes, "coverage_target");
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution", 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, axes,
reason: "planner_selected_entity_resolution_recipe" reason: "planner_selected_entity_resolution_recipe"
}; });
} }
return { return recipeFromCatalogChainTemplate({
semanticDataNeed: "unclassified 1C discovery need",
chainId: "metadata_inspection", 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.", 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, axes,
reason: "planner_selected_clarification_recipe" reason: "planner_selected_clarification_recipe"
}; });
} }
function statusFrom( 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 { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } from "../src/services/assistantMcpDiscoveryPolicy";
import { import {
buildAssistantMcpCatalogIndex, buildAssistantMcpCatalogIndex,
getAssistantMcpCatalogChainTemplate,
getAssistantMcpCatalogPrimitive, getAssistantMcpCatalogPrimitive,
reviewAssistantMcpDiscoveryPlanAgainstCatalog, reviewAssistantMcpDiscoveryPlanAgainstCatalog,
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates, searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
@ -15,6 +16,7 @@ describe("assistant MCP catalog index", () => {
const primitiveIds = index.primitives.map((entry) => entry.primitive_id); 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_covers_all_discovery_primitives");
expect(index.reason_codes).toContain("catalog_chain_templates_reference_reviewed_primitives");
expect(primitiveIds).toEqual([...ASSISTANT_MCP_DISCOVERY_PRIMITIVES]); expect(primitiveIds).toEqual([...ASSISTANT_MCP_DISCOVERY_PRIMITIVES]);
for (const entry of index.primitives) { for (const entry of index.primitives) {
expect(entry.safe_for_model_planning).toBe(true); 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.required_axes_any_of.length).toBeGreaterThan(0);
expect(entry.output_fact_kinds.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", () => { 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.selected_chain_id).toBe("document_evidence");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]); 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_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"); 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.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_movement_from_data_need_graph");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_decomposition_candidates"); 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", () => { 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.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("entity_resolution"); expect(result.selected_chain_id).toBe("entity_resolution");
expect(result.reason_codes).toContain("planner_selected_entity_resolution_recipe"); 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_document_from_confirmed_metadata_surface_ref");
expect(result.reason_codes).not.toContain("planner_selected_movement_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" "calendar_month"
]); ]);
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_from_data_need_graph"); 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", () => { 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.required_axes).toEqual(["period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes"); 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_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"); 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.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible"); 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_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", () => { 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.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "amount", "coverage_target"]); 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_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"); 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.required_axes).toEqual(["organization", "period", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible"); 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_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", () => { 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.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible"); 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_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", () => { it("keeps generic one-sided open totals in organization clarification instead of forcing entity resolution", () => {
const result = planAssistantMcpDiscovery({ 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.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["organization"]); 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_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"); 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.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_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", () => { it("keeps metadata-scoped movement evidence in clarification instead of forcing entity resolution", () => {