ARCH: заложить контракт MCP semantic discovery

This commit is contained in:
2026-04-20 09:01:14 +03:00
parent 05aad66dc4
commit a75da40178
4 changed files with 872 additions and 0 deletions
@@ -0,0 +1,141 @@
import { describe, expect, it } from "vitest";
import {
buildAssistantMcpDiscoveryPlan,
isAssistantMcpDiscoveryPrimitive,
resolveAssistantMcpDiscoveryEvidence
} from "../src/services/assistantMcpDiscoveryPolicy";
describe("assistant MCP discovery policy", () => {
it("allows guarded MCP primitives and keeps raw model claims outside the answer path", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "counterparty turnover evidence",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
},
proposedPrimitives: ["resolve_entity_reference", "query_movements", "drop_database"],
requiredAxes: ["counterparty", "period", "amount"],
maxProbeCount: 99,
maxRowsPerProbe: 9999
});
expect(plan.plan_status).toBe("allowed");
expect(plan.allowed_primitives).toEqual(["resolve_entity_reference", "query_movements"]);
expect(plan.rejected_primitives).toEqual(["drop_database"]);
expect(plan.requires_evidence_gate).toBe(true);
expect(plan.answer_may_use_raw_model_claims).toBe(false);
expect(plan.execution_budget).toEqual({ max_probe_count: 6, max_rows_per_probe: 500 });
expect(plan.reason_codes).toContain("model_proposed_unregistered_mcp_primitive");
});
it("blocks model-planned probes when no proposed primitive survives the runtime allowlist", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "direct SQL from model",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["СВК"]
},
proposedPrimitives: ["raw_sql", "filesystem_read"],
requiredAxes: ["counterparty"]
});
expect(plan.plan_status).toBe("blocked");
expect(plan.allowed_primitives).toEqual([]);
expect(plan.rejected_primitives).toEqual(["raw_sql", "filesystem_read"]);
expect(plan.reason_codes).toContain("no_allowed_mcp_primitives_after_runtime_filter");
});
it("separates confirmed, inferred and unknown facts before answer composition", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "activity duration from 1C evidence",
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["СВК"]
},
proposedPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
requiredAxes: ["counterparty", "document_date"]
});
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan,
probeResults: [
{ primitive_id: "resolve_entity_reference", status: "ok", rows_received: 1, rows_matched: 1 },
{ primitive_id: "query_documents", status: "ok", rows_received: 4, rows_matched: 4 }
],
confirmedFacts: ["first confirmed 1C activity is 2020-01-15"],
inferredFacts: ["activity duration can be estimated from first and latest 1C activity"],
unknownFacts: ["legal registration date is not proven by these rows"],
sourceRowsSummary: "5 allowed MCP rows: 1 entity match, 4 documents"
});
expect(evidence.evidence_status).toBe("confirmed");
expect(evidence.coverage_status).toBe("full");
expect(evidence.answer_permission).toBe("confirmed_answer");
expect(evidence.confirmed_facts).toHaveLength(1);
expect(evidence.inferred_facts).toHaveLength(1);
expect(evidence.unknown_facts).toHaveLength(1);
expect(evidence.reason_codes).toContain("confirmed_facts_with_allowed_mcp_evidence");
});
it("permits only bounded inference when probes found rows but no confirmed fact", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "counterparty business age inference",
turnMeaning: {
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "age_or_activity_duration",
explicit_entity_candidates: ["СВК"]
},
proposedPrimitives: ["query_documents"],
requiredAxes: ["counterparty", "document_date"]
});
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan,
probeResults: [{ primitive_id: "query_documents", status: "ok", rows_received: 3, rows_matched: 0 }],
inferredFacts: ["activity is visible in 1C documents for 2020"],
unknownFacts: ["legal age remains unknown"],
sourceRowsSummary: "3 document rows checked"
});
expect(evidence.evidence_status).toBe("inferred_only");
expect(evidence.coverage_status).toBe("partial");
expect(evidence.answer_permission).toBe("bounded_inference");
expect(evidence.confidence_reason).toBe("only_inferred_facts_available_from_allowed_mcp_probe_rows");
});
it("blocks evidence when execution reports a primitive outside the runtime plan", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "counterparty turnover evidence",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["СВК"]
},
proposedPrimitives: ["query_movements"],
requiredAxes: ["counterparty"]
});
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan,
probeResults: [
{ primitive_id: "query_movements", status: "ok", rows_received: 2, rows_matched: 2 },
{ primitive_id: "raw_sql", status: "ok", rows_received: 10, rows_matched: 10 }
],
confirmedFacts: ["turnover is 100"],
sourceRowsSummary: "12 rows"
});
expect(evidence.evidence_status).toBe("blocked");
expect(evidence.answer_permission).toBe("checked_sources_only");
expect(evidence.reason_codes).toContain("probe_result_used_primitive_outside_runtime_plan");
});
it("exports the reviewed primitive predicate for future runtime adapters", () => {
expect(isAssistantMcpDiscoveryPrimitive("query_movements")).toBe(true);
expect(isAssistantMcpDiscoveryPrimitive("raw_sql")).toBe(false);
});
});