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