ARCH: добавить MCP catalog index для discovery
This commit is contained in:
parent
a75da40178
commit
1752d1babf
|
|
@ -682,6 +682,35 @@ Validation:
|
|||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts` passed 6/6;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Progress Update - 2026-04-20 MCP Catalog Index Seed
|
||||
|
||||
The second implementation slice of Big Block 5 added the first machine-readable catalog brain for guarded MCP discovery:
|
||||
|
||||
- `assistantMcpCatalogIndex.ts`
|
||||
- `assistantMcpCatalogIndex.test.ts`
|
||||
|
||||
The catalog does not execute 1C/MCP calls yet.
|
||||
|
||||
It records what each reviewed discovery primitive is allowed to mean:
|
||||
|
||||
- business purpose;
|
||||
- required grounding axes;
|
||||
- optional axes;
|
||||
- output fact kinds;
|
||||
- minimum evidence floor.
|
||||
|
||||
This gives future planner/executor wiring a deterministic policy surface:
|
||||
|
||||
- every discovery primitive must have a catalog contract;
|
||||
- a discovery plan can be reviewed against catalog-required axes before execution;
|
||||
- missing axes degrade to `needs_more_axes` instead of a blind query;
|
||||
- blocked discovery plans remain blocked at catalog review level.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts` passed 11/11;
|
||||
- `npm run build` passed.
|
||||
|
||||
## Execution Rule
|
||||
|
||||
Do not implement this plan as:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
|
||||
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
|
||||
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = "assistant_mcp_catalog_index_v1";
|
||||
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = "assistant_mcp_catalog_plan_review_v1";
|
||||
const PRIMITIVE_CONTRACTS = [
|
||||
{
|
||||
primitive_id: "search_business_entity",
|
||||
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "document", "account"],
|
||||
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
|
||||
evidence_floor: "rows_received",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "inspect_1c_metadata",
|
||||
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
|
||||
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
|
||||
optional_axes: ["business_entity", "account", "counterparty"],
|
||||
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "resolve_entity_reference",
|
||||
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
|
||||
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
|
||||
optional_axes: ["period", "inn", "document"],
|
||||
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "query_movements",
|
||||
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
|
||||
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
|
||||
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "query_documents",
|
||||
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
|
||||
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
|
||||
optional_axes: ["account", "amount", "item", "warehouse"],
|
||||
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "aggregate_by_axis",
|
||||
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
|
||||
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
|
||||
optional_axes: ["organization", "contract", "document", "amount"],
|
||||
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
|
||||
evidence_floor: "rows_matched",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "drilldown_related_objects",
|
||||
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
|
||||
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
|
||||
optional_axes: ["period", "account", "amount"],
|
||||
output_fact_kinds: ["related_objects", "relationship_edges"],
|
||||
evidence_floor: "rows_received",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "probe_coverage",
|
||||
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
|
||||
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
|
||||
optional_axes: ["period", "organization", "counterparty", "document", "account"],
|
||||
output_fact_kinds: ["coverage_status", "known_gaps"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
},
|
||||
{
|
||||
primitive_id: "explain_evidence_basis",
|
||||
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
|
||||
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
|
||||
optional_axes: ["coverage_target", "domain_family"],
|
||||
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
|
||||
evidence_floor: "source_summary",
|
||||
safe_for_model_planning: true,
|
||||
runtime_must_execute: true
|
||||
}
|
||||
];
|
||||
const PRIMITIVE_CONTRACT_MAP = new Map(PRIMITIVE_CONTRACTS.map((contract) => [contract.primitive_id, contract]));
|
||||
function toStringSet(values) {
|
||||
return new Set(values.map((item) => item.trim()).filter((item) => item.length > 0));
|
||||
}
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||
}
|
||||
function pushReason(target, value) {
|
||||
const normalized = normalizeReasonCode(value);
|
||||
if (normalized && !target.includes(normalized)) {
|
||||
target.push(normalized);
|
||||
}
|
||||
}
|
||||
function hasAnyAxisGroup(axisSet, groups) {
|
||||
return groups.some((group) => group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
function missingAxisGroups(axisSet, groups) {
|
||||
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
|
||||
}
|
||||
function buildAssistantMcpCatalogIndex() {
|
||||
const reasonCodes = [];
|
||||
const missingContracts = assistantMcpDiscoveryPolicy_1.ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));
|
||||
if (missingContracts.length > 0) {
|
||||
pushReason(reasonCodes, "catalog_missing_discovery_primitive_contract");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "catalog_covers_all_discovery_primitives");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpCatalogIndex",
|
||||
primitives: PRIMITIVE_CONTRACTS,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
function getAssistantMcpCatalogPrimitive(primitive) {
|
||||
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
|
||||
if (!contract) {
|
||||
throw new Error(`Missing MCP catalog primitive contract: ${primitive}`);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
function reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan) {
|
||||
const reasonCodes = [];
|
||||
const axisSet = toStringSet(plan.required_axes);
|
||||
const reviewedPrimitives = [];
|
||||
const missingAxesByPrimitive = {};
|
||||
const unknownPrimitives = [];
|
||||
const evidenceFloors = {};
|
||||
for (const primitive of plan.allowed_primitives) {
|
||||
const contract = PRIMITIVE_CONTRACT_MAP.get(primitive);
|
||||
if (!contract) {
|
||||
unknownPrimitives.push(primitive);
|
||||
continue;
|
||||
}
|
||||
reviewedPrimitives.push(primitive);
|
||||
evidenceFloors[primitive] = contract.evidence_floor;
|
||||
if (!hasAnyAxisGroup(axisSet, contract.required_axes_any_of)) {
|
||||
missingAxesByPrimitive[primitive] = missingAxisGroups(axisSet, contract.required_axes_any_of);
|
||||
}
|
||||
}
|
||||
if (unknownPrimitives.length > 0) {
|
||||
pushReason(reasonCodes, "catalog_unknown_primitive_in_discovery_plan");
|
||||
}
|
||||
if (Object.keys(missingAxesByPrimitive).length > 0) {
|
||||
pushReason(reasonCodes, "catalog_required_axes_missing_for_primitive");
|
||||
}
|
||||
if (plan.plan_status !== "allowed") {
|
||||
pushReason(reasonCodes, "catalog_review_received_non_allowed_plan");
|
||||
}
|
||||
let reviewStatus = "catalog_compatible";
|
||||
if (unknownPrimitives.length > 0 || plan.plan_status === "blocked") {
|
||||
reviewStatus = "catalog_blocked";
|
||||
}
|
||||
else if (plan.plan_status !== "allowed" || Object.keys(missingAxesByPrimitive).length > 0) {
|
||||
reviewStatus = "needs_more_axes";
|
||||
}
|
||||
if (reviewStatus === "catalog_compatible") {
|
||||
pushReason(reasonCodes, "catalog_plan_compatible");
|
||||
}
|
||||
else if (reviewStatus === "catalog_blocked") {
|
||||
pushReason(reasonCodes, "catalog_plan_blocked");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "catalog_plan_needs_more_axes");
|
||||
}
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpCatalogIndex",
|
||||
review_status: reviewStatus,
|
||||
reviewed_primitives: reviewedPrimitives,
|
||||
missing_axes_by_primitive: missingAxesByPrimitive,
|
||||
unknown_primitives: unknownPrimitives,
|
||||
evidence_floors: evidenceFloors,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } from "../src/services/assistantMcpDiscoveryPolicy";
|
||||
import {
|
||||
buildAssistantMcpCatalogIndex,
|
||||
getAssistantMcpCatalogPrimitive,
|
||||
reviewAssistantMcpDiscoveryPlanAgainstCatalog
|
||||
} from "../src/services/assistantMcpCatalogIndex";
|
||||
|
||||
describe("assistant MCP catalog index", () => {
|
||||
it("declares a catalog contract for every reviewed discovery primitive", () => {
|
||||
const index = buildAssistantMcpCatalogIndex();
|
||||
const primitiveIds = index.primitives.map((entry) => entry.primitive_id);
|
||||
|
||||
expect(index.reason_codes).toContain("catalog_covers_all_discovery_primitives");
|
||||
expect(primitiveIds).toEqual([...ASSISTANT_MCP_DISCOVERY_PRIMITIVES]);
|
||||
for (const entry of index.primitives) {
|
||||
expect(entry.safe_for_model_planning).toBe(true);
|
||||
expect(entry.runtime_must_execute).toBe(true);
|
||||
expect(entry.required_axes_any_of.length).toBeGreaterThan(0);
|
||||
expect(entry.output_fact_kinds.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("marks a counterparty turnover discovery plan as catalog-compatible when required axes exist", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "counterparty turnover evidence",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
proposedPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis"],
|
||||
requiredAxes: ["counterparty", "period", "aggregate_axis", "amount"]
|
||||
});
|
||||
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
||||
expect(review.review_status).toBe("catalog_compatible");
|
||||
expect(review.reason_codes).toContain("catalog_plan_compatible");
|
||||
expect(review.missing_axes_by_primitive).toEqual({});
|
||||
expect(review.evidence_floors.query_movements).toBe("rows_matched");
|
||||
});
|
||||
|
||||
it("asks for more axes before document discovery can run safely", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "document evidence",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_documents",
|
||||
asked_action_family: "list_documents"
|
||||
},
|
||||
proposedPrimitives: ["query_documents"],
|
||||
requiredAxes: ["amount"]
|
||||
});
|
||||
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
||||
expect(review.review_status).toBe("needs_more_axes");
|
||||
expect(review.reason_codes).toContain("catalog_required_axes_missing_for_primitive");
|
||||
expect(review.missing_axes_by_primitive.query_documents).toEqual([
|
||||
["document"],
|
||||
["counterparty"],
|
||||
["contract"],
|
||||
["period", "organization"]
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves source-summary evidence floors for metadata and coverage primitives", () => {
|
||||
expect(getAssistantMcpCatalogPrimitive("inspect_1c_metadata").evidence_floor).toBe("source_summary");
|
||||
expect(getAssistantMcpCatalogPrimitive("probe_coverage").evidence_floor).toBe("source_summary");
|
||||
expect(getAssistantMcpCatalogPrimitive("resolve_entity_reference").evidence_floor).toBe("rows_matched");
|
||||
});
|
||||
|
||||
it("turns a non-allowed discovery plan into a catalog-level blocked review", () => {
|
||||
const plan = buildAssistantMcpDiscoveryPlan({
|
||||
semanticDataNeed: "raw model sql",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
},
|
||||
proposedPrimitives: ["raw_sql"],
|
||||
requiredAxes: ["counterparty"]
|
||||
});
|
||||
|
||||
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
|
||||
|
||||
expect(plan.plan_status).toBe("blocked");
|
||||
expect(review.review_status).toBe("catalog_blocked");
|
||||
expect(review.reason_codes).toContain("catalog_review_received_non_allowed_plan");
|
||||
expect(review.reason_codes).toContain("catalog_plan_blocked");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue