Compare commits
3 Commits
43d50e21cc
...
a75da40178
| Author | SHA1 | Date |
|---|---|---|
|
|
a75da40178 | |
|
|
05aad66dc4 | |
|
|
399e29f4a4 |
|
|
@ -373,9 +373,156 @@ This block is complete only when:
|
||||||
- truthful limited answers do not look like stale replay;
|
- truthful limited answers do not look like stale replay;
|
||||||
- human answer quality becomes a structural acceptance dimension, not a soft preference.
|
- human answer quality becomes a structural acceptance dimension, not a soft preference.
|
||||||
|
|
||||||
|
## Big Block 5. MCP Semantic Data Agent Instead Of Route Hardcoding
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Reduce the need to hardcode every new business question as a separate route by introducing a guarded semantic data-discovery layer over 1C/MCP.
|
||||||
|
|
||||||
|
This block does not mean giving Qwen3 unrestricted authority to invent arbitrary 1C queries.
|
||||||
|
|
||||||
|
It means letting the model help build and revise a data-search plan while deterministic runtime contracts still own:
|
||||||
|
|
||||||
|
- allowed MCP primitives;
|
||||||
|
- schema/catalog boundaries;
|
||||||
|
- execution budgets;
|
||||||
|
- evidence sufficiency;
|
||||||
|
- final answer truthfulness.
|
||||||
|
|
||||||
|
### Architectural Rule
|
||||||
|
|
||||||
|
The assistant may explore 1C data through MCP only through reviewed data primitives and evidence gates.
|
||||||
|
|
||||||
|
The model can propose:
|
||||||
|
|
||||||
|
- which business object to look for;
|
||||||
|
- which metric or evidence axis is needed;
|
||||||
|
- which period, organization, counterparty, contract, account, register, or document family should constrain the search;
|
||||||
|
- whether the first query result is sufficient or requires a follow-up probe.
|
||||||
|
|
||||||
|
The runtime must decide:
|
||||||
|
|
||||||
|
- whether the proposed search is allowed;
|
||||||
|
- which concrete MCP primitive or query template can execute it;
|
||||||
|
- whether returned evidence proves the answer, only supports an inference, or is insufficient;
|
||||||
|
- how the answer should describe confirmed facts, inferred facts, and unknowns.
|
||||||
|
|
||||||
|
### Required Shift
|
||||||
|
|
||||||
|
The route layer should stop being the only way to reach live 1C data.
|
||||||
|
|
||||||
|
Today, the common pattern is:
|
||||||
|
|
||||||
|
- wording signal;
|
||||||
|
- fixed intent;
|
||||||
|
- fixed route/capability;
|
||||||
|
- fixed query/reply branch.
|
||||||
|
|
||||||
|
The target pattern is:
|
||||||
|
|
||||||
|
- current-turn meaning authority;
|
||||||
|
- semantic data need;
|
||||||
|
- guarded MCP discovery plan;
|
||||||
|
- evidence object;
|
||||||
|
- answer contract.
|
||||||
|
|
||||||
|
Exact routes remain valuable for hot, high-confidence contours.
|
||||||
|
|
||||||
|
But new or long-tail business questions should be able to enter a controlled discovery lane instead of immediately becoming:
|
||||||
|
|
||||||
|
- unsupported;
|
||||||
|
- stale carryover;
|
||||||
|
- or another hand-coded route request.
|
||||||
|
|
||||||
|
### MCP Primitive Families
|
||||||
|
|
||||||
|
The discovery lane should expose a small set of broad, reviewed primitives rather than many free-form model tools:
|
||||||
|
|
||||||
|
- `search_business_entity`
|
||||||
|
- `inspect_1c_metadata`
|
||||||
|
- `resolve_entity_reference`
|
||||||
|
- `query_movements`
|
||||||
|
- `query_documents`
|
||||||
|
- `aggregate_by_axis`
|
||||||
|
- `drilldown_related_objects`
|
||||||
|
- `probe_coverage`
|
||||||
|
- `explain_evidence_basis`
|
||||||
|
|
||||||
|
These are not final API names.
|
||||||
|
|
||||||
|
They describe the architectural shape: the model plans at business level, while runtime adapters execute controlled 1C/MCP operations.
|
||||||
|
|
||||||
|
### Required Catalog Brain
|
||||||
|
|
||||||
|
The assistant needs a machine-readable 1C schema/catalog memory before this can be safe:
|
||||||
|
|
||||||
|
- available catalogs, documents, registers, and accounting axes;
|
||||||
|
- known links between counterparties, contracts, documents, accounts, payments, shipments, and balances;
|
||||||
|
- safe query templates and field mappings;
|
||||||
|
- known MCP limitations and fallback probes;
|
||||||
|
- examples of proven query recipes from accepted semantic runs.
|
||||||
|
|
||||||
|
Without this catalog brain, a model-led MCP agent will guess.
|
||||||
|
|
||||||
|
Guessing is not acceptable for accounting answers.
|
||||||
|
|
||||||
|
### Truth And Evidence Requirements
|
||||||
|
|
||||||
|
Every discovery result must emit an evidence object before answer composition:
|
||||||
|
|
||||||
|
- `confirmed_facts`
|
||||||
|
- `inferred_facts`
|
||||||
|
- `unknown_facts`
|
||||||
|
- `source_rows_summary`
|
||||||
|
- `coverage_status`
|
||||||
|
- `query_plan`
|
||||||
|
- `query_limitations`
|
||||||
|
- `confidence_reason`
|
||||||
|
- `recommended_next_probe`
|
||||||
|
|
||||||
|
The final answer may not present an inference as a confirmed 1C fact.
|
||||||
|
|
||||||
|
If the exact fact is unavailable but a useful inference is possible from 1C activity evidence, the answer must say that clearly.
|
||||||
|
|
||||||
|
### Stack Mapping
|
||||||
|
|
||||||
|
Existing seams that already point in this direction:
|
||||||
|
|
||||||
|
- `AssistantDataLayer`
|
||||||
|
- `buildLiveMcpCallPlan`
|
||||||
|
- `buildSemanticRetrievalProfile`
|
||||||
|
- `addressMcpClient.ts`
|
||||||
|
- `AddressQueryService`
|
||||||
|
- truth/coverage/evidence contracts
|
||||||
|
|
||||||
|
Primary new owner candidates:
|
||||||
|
|
||||||
|
- `assistantSemanticDataAgentPolicy.ts`
|
||||||
|
- `assistantMcpDiscoveryPlanner.ts`
|
||||||
|
- `assistantMcpEvidenceGate.ts`
|
||||||
|
- `assistantMcpCatalogIndex.ts`
|
||||||
|
|
||||||
|
The naming can change, but the ownership split should not:
|
||||||
|
|
||||||
|
- planner proposes a business-level data plan;
|
||||||
|
- catalog constrains what can be searched;
|
||||||
|
- executor runs allowed MCP primitives;
|
||||||
|
- evidence gate decides what can be said;
|
||||||
|
- answer layer explains the result in human business terms.
|
||||||
|
|
||||||
|
### Done Criteria
|
||||||
|
|
||||||
|
This block is complete only when:
|
||||||
|
|
||||||
|
- at least one long-tail 1C business question can be answered through discovery without adding a one-off route branch;
|
||||||
|
- the discovery lane produces machine-readable query/evidence artifacts;
|
||||||
|
- failed discovery degrades to a useful "what I checked / what is still unknown" answer, not a generic unsupported fallback;
|
||||||
|
- exact hot routes and semantic discovery can coexist without route collisions;
|
||||||
|
- semantic replay can prove that the model does not leak internal query mechanics or hallucinate unconfirmed facts.
|
||||||
|
|
||||||
## Concrete Stack Plan
|
## Concrete Stack Plan
|
||||||
|
|
||||||
This problem should be addressed in the current stack through four large architecture blocks, not through many micro-passes.
|
This problem should be addressed in the current stack through five large architecture blocks, not through many micro-passes.
|
||||||
|
|
||||||
### Stack Block A. Turn Meaning Layer
|
### Stack Block A. Turn Meaning Layer
|
||||||
|
|
||||||
|
|
@ -441,6 +588,25 @@ Required result:
|
||||||
- top-block answer correctness becomes part of acceptance;
|
- top-block answer correctness becomes part of acceptance;
|
||||||
- "route technically matched" no longer overrules semantic mismatch.
|
- "route technically matched" no longer overrules semantic mismatch.
|
||||||
|
|
||||||
|
### Stack Block E. MCP Semantic Data Discovery Layer
|
||||||
|
|
||||||
|
Add a guarded discovery lane for business questions that are understood but not yet covered by an exact route.
|
||||||
|
|
||||||
|
Primary files and owner seams:
|
||||||
|
|
||||||
|
- [addressMcpClient.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressMcpClient.ts:1)
|
||||||
|
- [addressQueryService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressQueryService.ts:1)
|
||||||
|
- future `assistantMcpCatalogIndex.ts`
|
||||||
|
- future `assistantMcpDiscoveryPlanner.ts`
|
||||||
|
- future `assistantMcpEvidenceGate.ts`
|
||||||
|
|
||||||
|
Required result:
|
||||||
|
|
||||||
|
- Qwen3 may help plan MCP exploration, but it cannot directly define truth;
|
||||||
|
- runtime exposes guarded MCP primitives instead of arbitrary model-generated 1C access;
|
||||||
|
- every discovery answer is backed by an explicit evidence object;
|
||||||
|
- long-tail understood business questions become recoverable without route-per-question hardcoding.
|
||||||
|
|
||||||
## Required Acceptance Invariants
|
## Required Acceptance Invariants
|
||||||
|
|
||||||
The architecture should not be considered corrected until the following invariants are green:
|
The architecture should not be considered corrected until the following invariants are green:
|
||||||
|
|
@ -453,6 +619,10 @@ The architecture should not be considered corrected until the following invarian
|
||||||
6. `short_followup_retains_dialog_stem_without_glitch_replay`
|
6. `short_followup_retains_dialog_stem_without_glitch_replay`
|
||||||
7. `answer_top_block_matches_current_user_intent`
|
7. `answer_top_block_matches_current_user_intent`
|
||||||
8. `meta_interrupt_does_not_corrupt_business_thread`
|
8. `meta_interrupt_does_not_corrupt_business_thread`
|
||||||
|
9. `understood_long_tail_question_enters_guarded_mcp_discovery`
|
||||||
|
10. `mcp_discovery_answer_separates_confirmed_inferred_and_unknown_facts`
|
||||||
|
11. `model_planned_mcp_probe_cannot_bypass_runtime_evidence_gate`
|
||||||
|
12. `failed_discovery_reports_checked_sources_without_hallucinated_fact`
|
||||||
|
|
||||||
## Progress Update - 2026-04-20
|
## Progress Update - 2026-04-20
|
||||||
|
|
||||||
|
|
@ -474,6 +644,44 @@ Important semantic conclusion:
|
||||||
- the turn `какой оборот был свк` now routes to `customer_revenue_and_payments` and opens with direct turnover for `Группа СВК`, not with stale `Чепурнов` documents or a generic top-client ranking;
|
- the turn `какой оборот был свк` now routes to `customer_revenue_and_payments` and opens with direct turnover for `Группа СВК`, not with stale `Чепурнов` documents or a generic top-client ranking;
|
||||||
- off-domain living-chat turns after a business answer are accepted when they stay live and do not replay stale business context.
|
- off-domain living-chat turns after a business answer are accepted when they stay live and do not replay stale business context.
|
||||||
|
|
||||||
|
## Progress Update - 2026-04-20 Contract Binding Cleanup
|
||||||
|
|
||||||
|
The same phase18 replay was rerun after adding runtime contracts for the counterparty document and value-flow capabilities:
|
||||||
|
|
||||||
|
- live replay artifact: `artifacts/domain_runs/address_truth_harness_phase18_contract_binding_rerun`
|
||||||
|
- `final_status`: `accepted`
|
||||||
|
- `steps_passed`: 7/7
|
||||||
|
|
||||||
|
Contract-layer result:
|
||||||
|
|
||||||
|
- `documents_drilldown`: `transition_contract_id=T1`, `capability_binding_status=bound`, no binding violations;
|
||||||
|
- `address_customer_revenue_and_payments`: `transition_contract_id=T1`, `capability_binding_status=bound`, no binding violations.
|
||||||
|
|
||||||
|
This closes the misleading `capability_contract_missing` / `transition_contract_not_resolved` debug gap for the core phase18 path without changing the user-facing answer semantics.
|
||||||
|
|
||||||
|
## Progress Update - 2026-04-20 MCP Discovery Contract Seed
|
||||||
|
|
||||||
|
The first implementation slice of Big Block 5 added a non-runtime-disruptive contract owner for guarded MCP semantic discovery:
|
||||||
|
|
||||||
|
- `assistantMcpDiscoveryPolicy.ts`
|
||||||
|
- `assistantMcpDiscoveryPolicy.test.ts`
|
||||||
|
|
||||||
|
This seed does not yet route live user traffic into discovery.
|
||||||
|
|
||||||
|
It establishes the safety contract that future runtime wiring must obey:
|
||||||
|
|
||||||
|
- Qwen3 may propose business-level MCP exploration primitives;
|
||||||
|
- unregistered primitives are rejected by runtime policy;
|
||||||
|
- raw model claims are never answer-authoritative;
|
||||||
|
- every discovery answer must pass an evidence gate;
|
||||||
|
- evidence must separate confirmed facts, inferred facts, and unknown facts;
|
||||||
|
- probe execution cannot bypass the runtime-approved primitive plan.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `npm test -- assistantMcpDiscoveryPolicy.test.ts` passed 6/6;
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
## Execution Rule
|
## Execution Rule
|
||||||
|
|
||||||
Do not implement this plan as:
|
Do not implement this plan as:
|
||||||
|
|
@ -488,7 +696,8 @@ Implement it as:
|
||||||
- one shared current-turn meaning authority;
|
- one shared current-turn meaning authority;
|
||||||
- one explicit arbitration rule between new meaning and continuity;
|
- one explicit arbitration rule between new meaning and continuity;
|
||||||
- stronger family-level semantic robustness for supported contours;
|
- stronger family-level semantic robustness for supported contours;
|
||||||
- answer and replay gates that prove the assistant now feels alive to a human user.
|
- answer and replay gates that prove the assistant now feels alive to a human user;
|
||||||
|
- guarded MCP semantic discovery for understood questions that do not deserve one-off route hardcoding.
|
||||||
|
|
||||||
## Bottom Line
|
## Bottom Line
|
||||||
|
|
||||||
|
|
@ -498,7 +707,8 @@ It fails because it still lacks a stable architecture for:
|
||||||
|
|
||||||
- recognizing the meaning of the current turn;
|
- recognizing the meaning of the current turn;
|
||||||
- subordinating continuity to that meaning;
|
- subordinating continuity to that meaning;
|
||||||
- and reflecting that meaning in the final user-visible answer.
|
- reflecting that meaning in the final user-visible answer;
|
||||||
|
- and discovering relevant 1C evidence through controlled MCP primitives when no exact route exists yet.
|
||||||
|
|
||||||
That is the next large architecture block.
|
That is the next large architecture block.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ Current honest status:
|
||||||
- replay breadth still narrower than the intended multi-domain rollout surface beyond the flagship and late-switch families
|
- replay breadth still narrower than the intended multi-domain rollout surface beyond the flagship and late-switch families
|
||||||
- remaining answer-semantics pressure inside `composeStage.ts` / `answerComposer.ts`
|
- remaining answer-semantics pressure inside `composeStage.ts` / `answerComposer.ts`
|
||||||
- insufficient semantic robustness on live user wording, especially short follow-up retarget, typo tolerance, and intent-faithful human answers
|
- insufficient semantic robustness on live user wording, especially short follow-up retarget, typo tolerance, and intent-faithful human answers
|
||||||
|
- no guarded MCP semantic discovery lane yet for understood long-tail 1C questions that should not require one-off route hardcoding
|
||||||
|
|
||||||
Latest live proof now includes:
|
Latest live proof now includes:
|
||||||
|
|
||||||
|
|
@ -77,7 +78,7 @@ Current architectural reading:
|
||||||
- the system is already materially past the dangerous regression breakpoint;
|
- the system is already materially past the dangerous regression breakpoint;
|
||||||
- it is now safe for continued architecture hardening and controlled domain-by-domain enablement under replay gates;
|
- it is now safe for continued architecture hardening and controlled domain-by-domain enablement under replay gates;
|
||||||
- it is now materially closer to pre-multidomain stability, but still not safe to declare broad low-risk multi-domain expansion.
|
- it is now materially closer to pre-multidomain stability, but still not safe to declare broad low-risk multi-domain expansion.
|
||||||
- the practical next target is now `90%+ pre-multidomain readiness`, and the remaining gap should be treated as four large architecture iterations rather than as cosmetic cleanup.
|
- the practical next target is now `90%+ pre-multidomain readiness`, and the remaining gap should be treated as five large architecture iterations rather than as cosmetic cleanup.
|
||||||
- from this point onward, readiness must be judged not only by route truth and replay pass rate, but also by whether a new human user would feel that the assistant understands the intent and responds meaningfully in live wording.
|
- from this point onward, readiness must be judged not only by route truth and replay pass rate, but also by whether a new human user would feel that the assistant understands the intent and responds meaningfully in live wording.
|
||||||
|
|
||||||
For the detailed audit, current percentages, and remaining debt, read:
|
For the detailed audit, current percentages, and remaining debt, read:
|
||||||
|
|
@ -151,3 +152,4 @@ The biggest remaining blockers are:
|
||||||
- central intent pressure in `resolveAddressIntent()`;
|
- central intent pressure in `resolveAddressIntent()`;
|
||||||
- remaining answer-semantics pressure in `composeStage.ts` and `answerComposer.ts`.
|
- remaining answer-semantics pressure in `composeStage.ts` and `answerComposer.ts`.
|
||||||
- semantic robustness gaps where already-supported questions can still look broken to a human user because of typo sensitivity, short follow-up retarget loss, or human-answer mismatch.
|
- semantic robustness gaps where already-supported questions can still look broken to a human user because of typo sensitivity, short follow-up retarget loss, or human-answer mismatch.
|
||||||
|
- missing MCP semantic data-discovery layer where Qwen3 can help plan controlled 1C evidence search without bypassing runtime truth gates.
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,18 @@
|
||||||
"capability_layer": "compute",
|
"capability_layer": "compute",
|
||||||
"capability_route_mode": "exact"
|
"capability_route_mode": "exact"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"intent": "customer_revenue_and_payments",
|
||||||
|
"capability_id": "address_customer_revenue_and_payments",
|
||||||
|
"capability_layer": "compute",
|
||||||
|
"capability_route_mode": "exact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "supplier_payouts_profile",
|
||||||
|
"capability_id": "address_supplier_payouts_profile",
|
||||||
|
"capability_layer": "compute",
|
||||||
|
"capability_route_mode": "exact"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"intent": "list_documents_by_counterparty",
|
"intent": "list_documents_by_counterparty",
|
||||||
"capability_id": "documents_drilldown",
|
"capability_id": "documents_drilldown",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ const COMPUTE_EXACT_INTENTS = new Set([
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
"supplier_payouts_profile",
|
||||||
"open_contracts_confirmed_as_of_date",
|
"open_contracts_confirmed_as_of_date",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date",
|
||||||
|
|
@ -128,6 +130,14 @@ function resolveCapabilityEnabled(intent) {
|
||||||
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile") {
|
||||||
|
return {
|
||||||
|
enabled: config_1.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1,
|
||||||
|
reason: config_1.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1
|
||||||
|
? "counterparty_value_flow_route_enabled"
|
||||||
|
: "counterparty_value_flow_route_disabled_by_flag"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "inventory_on_hand_as_of_date") {
|
if (intent === "inventory_on_hand_as_of_date") {
|
||||||
return {
|
return {
|
||||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = exports.ASSISTANT_MCP_DISCOVERY_EVIDENCE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_PLAN_SCHEMA_VERSION = void 0;
|
||||||
|
exports.isAssistantMcpDiscoveryPrimitive = isAssistantMcpDiscoveryPrimitive;
|
||||||
|
exports.buildAssistantMcpDiscoveryPlan = buildAssistantMcpDiscoveryPlan;
|
||||||
|
exports.resolveAssistantMcpDiscoveryEvidence = resolveAssistantMcpDiscoveryEvidence;
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_PLAN_SCHEMA_VERSION = "assistant_mcp_discovery_plan_v1";
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_EVIDENCE_SCHEMA_VERSION = "assistant_mcp_discovery_evidence_v1";
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
|
||||||
|
"search_business_entity",
|
||||||
|
"inspect_1c_metadata",
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"query_movements",
|
||||||
|
"query_documents",
|
||||||
|
"aggregate_by_axis",
|
||||||
|
"drilldown_related_objects",
|
||||||
|
"probe_coverage",
|
||||||
|
"explain_evidence_basis"
|
||||||
|
];
|
||||||
|
const DEFAULT_DISCOVERY_BUDGET = {
|
||||||
|
max_probe_count: 3,
|
||||||
|
max_rows_per_probe: 100
|
||||||
|
};
|
||||||
|
const MAX_PROBE_COUNT = 6;
|
||||||
|
const MAX_ROWS_PER_PROBE = 500;
|
||||||
|
const ALLOWED_PRIMITIVE_SET = new Set(exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
|
||||||
|
function toNonEmptyString(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
function toStringList(value) {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result = [];
|
||||||
|
for (const item of value) {
|
||||||
|
const text = toNonEmptyString(item);
|
||||||
|
if (text && !result.includes(text)) {
|
||||||
|
result.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
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 clampInteger(value, fallback, min, max) {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Math.min(max, Math.max(min, Math.trunc(Number(value))));
|
||||||
|
}
|
||||||
|
function isAllowedPrimitive(value) {
|
||||||
|
return ALLOWED_PRIMITIVE_SET.has(value);
|
||||||
|
}
|
||||||
|
function normalizeTurnMeaning(value) {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const result = {};
|
||||||
|
const domain = toNonEmptyString(value.asked_domain_family);
|
||||||
|
const action = toNonEmptyString(value.asked_action_family);
|
||||||
|
const organization = toNonEmptyString(value.explicit_organization_scope);
|
||||||
|
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||||
|
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||||
|
const entities = toStringList(value.explicit_entity_candidates);
|
||||||
|
if (domain) {
|
||||||
|
result.asked_domain_family = domain;
|
||||||
|
}
|
||||||
|
if (action) {
|
||||||
|
result.asked_action_family = action;
|
||||||
|
}
|
||||||
|
if (entities.length > 0) {
|
||||||
|
result.explicit_entity_candidates = entities;
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
result.explicit_organization_scope = organization;
|
||||||
|
}
|
||||||
|
if (dateScope) {
|
||||||
|
result.explicit_date_scope = dateScope;
|
||||||
|
}
|
||||||
|
if (Number.isFinite(value.meaning_confidence)) {
|
||||||
|
result.meaning_confidence = Math.max(0, Math.min(1, Number(value.meaning_confidence)));
|
||||||
|
}
|
||||||
|
if (unsupported) {
|
||||||
|
result.unsupported_but_understood_family = unsupported;
|
||||||
|
}
|
||||||
|
if (value.stale_replay_forbidden !== null && value.stale_replay_forbidden !== undefined) {
|
||||||
|
result.stale_replay_forbidden = Boolean(value.stale_replay_forbidden);
|
||||||
|
}
|
||||||
|
return Object.keys(result).length > 0 ? result : null;
|
||||||
|
}
|
||||||
|
function hasGroundingAxis(input) {
|
||||||
|
if (input.requiredAxes.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const meaning = input.turnMeaning;
|
||||||
|
return Boolean(meaning?.asked_domain_family ||
|
||||||
|
meaning?.asked_action_family ||
|
||||||
|
meaning?.explicit_organization_scope ||
|
||||||
|
meaning?.explicit_date_scope ||
|
||||||
|
(meaning?.explicit_entity_candidates?.length ?? 0) > 0);
|
||||||
|
}
|
||||||
|
function isAssistantMcpDiscoveryPrimitive(value) {
|
||||||
|
return isAllowedPrimitive(value);
|
||||||
|
}
|
||||||
|
function buildAssistantMcpDiscoveryPlan(input) {
|
||||||
|
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed);
|
||||||
|
const turnMeaning = normalizeTurnMeaning(input.turnMeaning);
|
||||||
|
const requiredAxes = toStringList(input.requiredAxes);
|
||||||
|
const proposed = toStringList(input.proposedPrimitives);
|
||||||
|
const reasonCodes = [];
|
||||||
|
const allowedPrimitives = [];
|
||||||
|
const rejectedPrimitives = [];
|
||||||
|
for (const primitive of proposed) {
|
||||||
|
if (isAllowedPrimitive(primitive)) {
|
||||||
|
if (!allowedPrimitives.includes(primitive)) {
|
||||||
|
allowedPrimitives.push(primitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rejectedPrimitives.push(primitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rejectedPrimitives.length > 0) {
|
||||||
|
pushReason(reasonCodes, "model_proposed_unregistered_mcp_primitive");
|
||||||
|
}
|
||||||
|
if (!semanticDataNeed) {
|
||||||
|
pushReason(reasonCodes, "semantic_data_need_missing");
|
||||||
|
}
|
||||||
|
if (!turnMeaning) {
|
||||||
|
pushReason(reasonCodes, "turn_meaning_ref_missing");
|
||||||
|
}
|
||||||
|
if (!hasGroundingAxis({ turnMeaning, requiredAxes })) {
|
||||||
|
pushReason(reasonCodes, "grounding_axis_missing");
|
||||||
|
}
|
||||||
|
if (allowedPrimitives.length === 0 && proposed.length > 0) {
|
||||||
|
pushReason(reasonCodes, "no_allowed_mcp_primitives_after_runtime_filter");
|
||||||
|
}
|
||||||
|
if (allowedPrimitives.length === 0 && proposed.length === 0) {
|
||||||
|
pushReason(reasonCodes, "mcp_primitives_not_proposed");
|
||||||
|
}
|
||||||
|
let planStatus = "allowed";
|
||||||
|
if (allowedPrimitives.length === 0 && proposed.length > 0) {
|
||||||
|
planStatus = "blocked";
|
||||||
|
}
|
||||||
|
else if (!semanticDataNeed || !turnMeaning || !hasGroundingAxis({ turnMeaning, requiredAxes })) {
|
||||||
|
planStatus = "needs_clarification";
|
||||||
|
}
|
||||||
|
else if (allowedPrimitives.length === 0) {
|
||||||
|
planStatus = "needs_clarification";
|
||||||
|
}
|
||||||
|
if (planStatus === "allowed") {
|
||||||
|
pushReason(reasonCodes, "guarded_mcp_discovery_plan_allowed");
|
||||||
|
}
|
||||||
|
else if (planStatus === "blocked") {
|
||||||
|
pushReason(reasonCodes, "guarded_mcp_discovery_plan_blocked");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pushReason(reasonCodes, "guarded_mcp_discovery_plan_needs_clarification");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PLAN_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryPolicy",
|
||||||
|
plan_status: planStatus,
|
||||||
|
semantic_data_need: semanticDataNeed,
|
||||||
|
turn_meaning_ref: turnMeaning,
|
||||||
|
allowed_primitives: allowedPrimitives,
|
||||||
|
rejected_primitives: rejectedPrimitives,
|
||||||
|
required_axes: requiredAxes,
|
||||||
|
execution_budget: {
|
||||||
|
max_probe_count: clampInteger(input.maxProbeCount, DEFAULT_DISCOVERY_BUDGET.max_probe_count, 1, MAX_PROBE_COUNT),
|
||||||
|
max_rows_per_probe: clampInteger(input.maxRowsPerProbe, DEFAULT_DISCOVERY_BUDGET.max_rows_per_probe, 1, MAX_ROWS_PER_PROBE)
|
||||||
|
},
|
||||||
|
requires_evidence_gate: true,
|
||||||
|
answer_may_use_raw_model_claims: false,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function collectProbeLimitations(probeResults) {
|
||||||
|
const limitations = [];
|
||||||
|
for (const probe of probeResults) {
|
||||||
|
const limitation = toNonEmptyString(probe.limitation);
|
||||||
|
if (limitation && !limitations.includes(limitation)) {
|
||||||
|
limitations.push(limitation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return limitations;
|
||||||
|
}
|
||||||
|
function probeRowsMatched(probeResults) {
|
||||||
|
return probeResults.reduce((sum, probe) => {
|
||||||
|
const rows = Number(probe.rows_matched ?? 0);
|
||||||
|
return sum + (Number.isFinite(rows) && rows > 0 ? rows : 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
function probeRowsReceived(probeResults) {
|
||||||
|
return probeResults.reduce((sum, probe) => {
|
||||||
|
const rows = Number(probe.rows_received ?? 0);
|
||||||
|
return sum + (Number.isFinite(rows) && rows > 0 ? rows : 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
function hasProbeBypass(plan, probeResults) {
|
||||||
|
const allowed = new Set(plan.allowed_primitives);
|
||||||
|
return probeResults.some((probe) => !allowed.has(probe.primitive_id));
|
||||||
|
}
|
||||||
|
function confidenceReasonFor(status) {
|
||||||
|
if (status === "confirmed") {
|
||||||
|
return "confirmed_facts_backed_by_allowed_mcp_probe_rows";
|
||||||
|
}
|
||||||
|
if (status === "inferred_only") {
|
||||||
|
return "only_inferred_facts_available_from_allowed_mcp_probe_rows";
|
||||||
|
}
|
||||||
|
if (status === "blocked") {
|
||||||
|
return "runtime_evidence_gate_blocked_discovery_answer";
|
||||||
|
}
|
||||||
|
return "allowed_mcp_probes_did_not_produce_sufficient_evidence";
|
||||||
|
}
|
||||||
|
function resolveAssistantMcpDiscoveryEvidence(input) {
|
||||||
|
const probeResults = Array.isArray(input.probeResults) ? input.probeResults : [];
|
||||||
|
const confirmedFacts = toStringList(input.confirmedFacts);
|
||||||
|
const inferredFacts = toStringList(input.inferredFacts);
|
||||||
|
const unknownFacts = toStringList(input.unknownFacts);
|
||||||
|
const sourceRowsSummary = toNonEmptyString(input.sourceRowsSummary);
|
||||||
|
const queryLimitations = [
|
||||||
|
...toStringList(input.queryLimitations),
|
||||||
|
...collectProbeLimitations(probeResults)
|
||||||
|
].filter((item, index, all) => all.indexOf(item) === index);
|
||||||
|
const reasonCodes = [...input.plan.reason_codes];
|
||||||
|
const rowsMatched = probeRowsMatched(probeResults);
|
||||||
|
const rowsReceived = probeRowsReceived(probeResults);
|
||||||
|
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||||
|
if (bypassDetected) {
|
||||||
|
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||||
|
}
|
||||||
|
if (input.plan.plan_status !== "allowed") {
|
||||||
|
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
||||||
|
}
|
||||||
|
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
|
||||||
|
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
||||||
|
}
|
||||||
|
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||||
|
pushReason(reasonCodes, "source_rows_summary_missing");
|
||||||
|
}
|
||||||
|
let evidenceStatus = "insufficient";
|
||||||
|
let coverageStatus = "blocked";
|
||||||
|
let answerPermission = "checked_sources_only";
|
||||||
|
if (bypassDetected || input.plan.plan_status !== "allowed") {
|
||||||
|
evidenceStatus = "blocked";
|
||||||
|
coverageStatus = "blocked";
|
||||||
|
answerPermission = "checked_sources_only";
|
||||||
|
}
|
||||||
|
else if (confirmedFacts.length > 0 && rowsMatched > 0 && sourceRowsSummary) {
|
||||||
|
evidenceStatus = "confirmed";
|
||||||
|
coverageStatus = "full";
|
||||||
|
answerPermission = "confirmed_answer";
|
||||||
|
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
||||||
|
}
|
||||||
|
else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
||||||
|
evidenceStatus = "inferred_only";
|
||||||
|
coverageStatus = "partial";
|
||||||
|
answerPermission = "bounded_inference";
|
||||||
|
pushReason(reasonCodes, "inferred_facts_require_bounded_answer");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_evidence_insufficient");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_EVIDENCE_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryPolicy",
|
||||||
|
evidence_status: evidenceStatus,
|
||||||
|
coverage_status: coverageStatus,
|
||||||
|
answer_permission: answerPermission,
|
||||||
|
confirmed_facts: confirmedFacts,
|
||||||
|
inferred_facts: inferredFacts,
|
||||||
|
unknown_facts: unknownFacts,
|
||||||
|
source_rows_summary: sourceRowsSummary,
|
||||||
|
query_plan: input.plan,
|
||||||
|
query_limitations: queryLimitations,
|
||||||
|
confidence_reason: confidenceReasonFor(evidenceStatus),
|
||||||
|
recommended_next_probe: toNonEmptyString(input.recommendedNextProbe),
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -324,6 +324,39 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
exports.ROOT_EXACT_CAPABILITY_CONTRACTS = [
|
exports.ROOT_EXACT_CAPABILITY_CONTRACTS = [
|
||||||
|
rootExactCapability({
|
||||||
|
capability_id: "documents_drilldown",
|
||||||
|
domainId: "counterparty_documents",
|
||||||
|
intent_ids: ["list_documents_by_counterparty", "list_documents_by_contract"],
|
||||||
|
transitions: ["T1", "T2", "T6", "T7"],
|
||||||
|
requiredAnchors: [],
|
||||||
|
optionalAnchors: ["organization", "date_scope", "counterparty", "contract", "document_type"],
|
||||||
|
resultShape: "counterparty_or_contract_document_list",
|
||||||
|
answerObjectShape: "documents_drilldown_list",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "short_counterparty_retarget", "followup_date_carryover"]
|
||||||
|
}),
|
||||||
|
rootExactCapability({
|
||||||
|
capability_id: "address_customer_revenue_and_payments",
|
||||||
|
domainId: "counterparty_revenue",
|
||||||
|
intent_ids: ["customer_revenue_and_payments"],
|
||||||
|
transitions: ["T1", "T2", "T6", "T7"],
|
||||||
|
requiredAnchors: [],
|
||||||
|
optionalAnchors: ["organization", "date_scope", "counterparty", "contract"],
|
||||||
|
resultShape: "counterparty_revenue_payment_flow",
|
||||||
|
answerObjectShape: "counterparty_revenue_summary",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "short_counterparty_retarget", "answer_top_block_matches_current_user_intent"]
|
||||||
|
}),
|
||||||
|
rootExactCapability({
|
||||||
|
capability_id: "address_supplier_payouts_profile",
|
||||||
|
domainId: "counterparty_payouts",
|
||||||
|
intent_ids: ["supplier_payouts_profile"],
|
||||||
|
transitions: ["T1", "T2", "T6", "T7"],
|
||||||
|
requiredAnchors: [],
|
||||||
|
optionalAnchors: ["organization", "date_scope", "counterparty", "contract"],
|
||||||
|
resultShape: "supplier_payout_payment_flow",
|
||||||
|
answerObjectShape: "supplier_payout_summary",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "short_counterparty_retarget"]
|
||||||
|
}),
|
||||||
rootExactCapability({
|
rootExactCapability({
|
||||||
capability_id: "confirmed_payables_as_of_date",
|
capability_id: "confirmed_payables_as_of_date",
|
||||||
domainId: "counterparty_debt",
|
domainId: "counterparty_debt",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
"customer_revenue_and_payments",
|
||||||
|
"supplier_payouts_profile",
|
||||||
"open_contracts_confirmed_as_of_date",
|
"open_contracts_confirmed_as_of_date",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date",
|
||||||
|
|
@ -154,6 +156,14 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
|
||||||
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "customer_revenue_and_payments" || intent === "supplier_payouts_profile") {
|
||||||
|
return {
|
||||||
|
enabled: FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1,
|
||||||
|
reason: FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1
|
||||||
|
? "counterparty_value_flow_route_enabled"
|
||||||
|
: "counterparty_value_flow_route_disabled_by_flag"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "inventory_on_hand_as_of_date") {
|
if (intent === "inventory_on_hand_as_of_date") {
|
||||||
return {
|
return {
|
||||||
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,411 @@
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_PLAN_SCHEMA_VERSION = "assistant_mcp_discovery_plan_v1" as const;
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_EVIDENCE_SCHEMA_VERSION = "assistant_mcp_discovery_evidence_v1" as const;
|
||||||
|
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
|
||||||
|
"search_business_entity",
|
||||||
|
"inspect_1c_metadata",
|
||||||
|
"resolve_entity_reference",
|
||||||
|
"query_movements",
|
||||||
|
"query_documents",
|
||||||
|
"aggregate_by_axis",
|
||||||
|
"drilldown_related_objects",
|
||||||
|
"probe_coverage",
|
||||||
|
"explain_evidence_basis"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type AssistantMcpDiscoveryPrimitive = (typeof ASSISTANT_MCP_DISCOVERY_PRIMITIVES)[number];
|
||||||
|
|
||||||
|
export type AssistantMcpDiscoveryPlanStatus = "allowed" | "needs_clarification" | "blocked";
|
||||||
|
export type AssistantMcpDiscoveryCoverageStatus = "full" | "partial" | "blocked";
|
||||||
|
export type AssistantMcpDiscoveryEvidenceStatus = "confirmed" | "inferred_only" | "insufficient" | "blocked";
|
||||||
|
export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounded_inference" | "checked_sources_only";
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryTurnMeaningRef {
|
||||||
|
asked_domain_family?: string | null;
|
||||||
|
asked_action_family?: string | null;
|
||||||
|
explicit_entity_candidates?: string[];
|
||||||
|
explicit_organization_scope?: string | null;
|
||||||
|
explicit_date_scope?: string | null;
|
||||||
|
meaning_confidence?: number | null;
|
||||||
|
unsupported_but_understood_family?: string | null;
|
||||||
|
stale_replay_forbidden?: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryExecutionBudget {
|
||||||
|
max_probe_count: number;
|
||||||
|
max_rows_per_probe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryPlanContract {
|
||||||
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_PLAN_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantMcpDiscoveryPolicy";
|
||||||
|
plan_status: AssistantMcpDiscoveryPlanStatus;
|
||||||
|
semantic_data_need: string | null;
|
||||||
|
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
|
allowed_primitives: AssistantMcpDiscoveryPrimitive[];
|
||||||
|
rejected_primitives: string[];
|
||||||
|
required_axes: string[];
|
||||||
|
execution_budget: AssistantMcpDiscoveryExecutionBudget;
|
||||||
|
requires_evidence_gate: true;
|
||||||
|
answer_may_use_raw_model_claims: false;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantMcpDiscoveryPlanInput {
|
||||||
|
semanticDataNeed?: string | null;
|
||||||
|
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
|
proposedPrimitives?: string[] | null;
|
||||||
|
requiredAxes?: string[] | null;
|
||||||
|
maxProbeCount?: number | null;
|
||||||
|
maxRowsPerProbe?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryProbeResult {
|
||||||
|
primitive_id: string;
|
||||||
|
status: "ok" | "error" | "skipped";
|
||||||
|
rows_received?: number | null;
|
||||||
|
rows_matched?: number | null;
|
||||||
|
limitation?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveAssistantMcpDiscoveryEvidenceInput {
|
||||||
|
plan: AssistantMcpDiscoveryPlanContract;
|
||||||
|
probeResults?: AssistantMcpDiscoveryProbeResult[] | null;
|
||||||
|
confirmedFacts?: string[] | null;
|
||||||
|
inferredFacts?: string[] | null;
|
||||||
|
unknownFacts?: string[] | null;
|
||||||
|
sourceRowsSummary?: string | null;
|
||||||
|
queryLimitations?: string[] | null;
|
||||||
|
recommendedNextProbe?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryEvidenceContract {
|
||||||
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_EVIDENCE_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantMcpDiscoveryPolicy";
|
||||||
|
evidence_status: AssistantMcpDiscoveryEvidenceStatus;
|
||||||
|
coverage_status: AssistantMcpDiscoveryCoverageStatus;
|
||||||
|
answer_permission: AssistantMcpDiscoveryAnswerPermission;
|
||||||
|
confirmed_facts: string[];
|
||||||
|
inferred_facts: string[];
|
||||||
|
unknown_facts: string[];
|
||||||
|
source_rows_summary: string | null;
|
||||||
|
query_plan: AssistantMcpDiscoveryPlanContract;
|
||||||
|
query_limitations: string[];
|
||||||
|
confidence_reason: string;
|
||||||
|
recommended_next_probe: string | null;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
||||||
|
max_probe_count: 3,
|
||||||
|
max_rows_per_probe: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_PROBE_COUNT = 6;
|
||||||
|
const MAX_ROWS_PER_PROBE = 500;
|
||||||
|
|
||||||
|
const ALLOWED_PRIMITIVE_SET = new Set<string>(ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
|
||||||
|
|
||||||
|
function toNonEmptyString(value: unknown): string | null {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = String(value).trim();
|
||||||
|
return text.length > 0 ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStringList(value: unknown): string[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const item of value) {
|
||||||
|
const text = toNonEmptyString(item);
|
||||||
|
if (text && !result.includes(text)) {
|
||||||
|
result.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 clampInteger(value: number | null | undefined, fallback: number, min: number, max: number): number {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Math.min(max, Math.max(min, Math.trunc(Number(value))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedPrimitive(value: string): value is AssistantMcpDiscoveryPrimitive {
|
||||||
|
return ALLOWED_PRIMITIVE_SET.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTurnMeaning(
|
||||||
|
value: AssistantMcpDiscoveryTurnMeaningRef | null | undefined
|
||||||
|
): AssistantMcpDiscoveryTurnMeaningRef | null {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const result: AssistantMcpDiscoveryTurnMeaningRef = {};
|
||||||
|
const domain = toNonEmptyString(value.asked_domain_family);
|
||||||
|
const action = toNonEmptyString(value.asked_action_family);
|
||||||
|
const organization = toNonEmptyString(value.explicit_organization_scope);
|
||||||
|
const dateScope = toNonEmptyString(value.explicit_date_scope);
|
||||||
|
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
|
||||||
|
const entities = toStringList(value.explicit_entity_candidates);
|
||||||
|
if (domain) {
|
||||||
|
result.asked_domain_family = domain;
|
||||||
|
}
|
||||||
|
if (action) {
|
||||||
|
result.asked_action_family = action;
|
||||||
|
}
|
||||||
|
if (entities.length > 0) {
|
||||||
|
result.explicit_entity_candidates = entities;
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
result.explicit_organization_scope = organization;
|
||||||
|
}
|
||||||
|
if (dateScope) {
|
||||||
|
result.explicit_date_scope = dateScope;
|
||||||
|
}
|
||||||
|
if (Number.isFinite(value.meaning_confidence)) {
|
||||||
|
result.meaning_confidence = Math.max(0, Math.min(1, Number(value.meaning_confidence)));
|
||||||
|
}
|
||||||
|
if (unsupported) {
|
||||||
|
result.unsupported_but_understood_family = unsupported;
|
||||||
|
}
|
||||||
|
if (value.stale_replay_forbidden !== null && value.stale_replay_forbidden !== undefined) {
|
||||||
|
result.stale_replay_forbidden = Boolean(value.stale_replay_forbidden);
|
||||||
|
}
|
||||||
|
return Object.keys(result).length > 0 ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasGroundingAxis(input: {
|
||||||
|
turnMeaning: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
|
requiredAxes: string[];
|
||||||
|
}): boolean {
|
||||||
|
if (input.requiredAxes.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const meaning = input.turnMeaning;
|
||||||
|
return Boolean(
|
||||||
|
meaning?.asked_domain_family ||
|
||||||
|
meaning?.asked_action_family ||
|
||||||
|
meaning?.explicit_organization_scope ||
|
||||||
|
meaning?.explicit_date_scope ||
|
||||||
|
(meaning?.explicit_entity_candidates?.length ?? 0) > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAssistantMcpDiscoveryPrimitive(value: string): value is AssistantMcpDiscoveryPrimitive {
|
||||||
|
return isAllowedPrimitive(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssistantMcpDiscoveryPlan(
|
||||||
|
input: BuildAssistantMcpDiscoveryPlanInput
|
||||||
|
): AssistantMcpDiscoveryPlanContract {
|
||||||
|
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed);
|
||||||
|
const turnMeaning = normalizeTurnMeaning(input.turnMeaning);
|
||||||
|
const requiredAxes = toStringList(input.requiredAxes);
|
||||||
|
const proposed = toStringList(input.proposedPrimitives);
|
||||||
|
const reasonCodes: string[] = [];
|
||||||
|
const allowedPrimitives: AssistantMcpDiscoveryPrimitive[] = [];
|
||||||
|
const rejectedPrimitives: string[] = [];
|
||||||
|
|
||||||
|
for (const primitive of proposed) {
|
||||||
|
if (isAllowedPrimitive(primitive)) {
|
||||||
|
if (!allowedPrimitives.includes(primitive)) {
|
||||||
|
allowedPrimitives.push(primitive);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rejectedPrimitives.push(primitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rejectedPrimitives.length > 0) {
|
||||||
|
pushReason(reasonCodes, "model_proposed_unregistered_mcp_primitive");
|
||||||
|
}
|
||||||
|
if (!semanticDataNeed) {
|
||||||
|
pushReason(reasonCodes, "semantic_data_need_missing");
|
||||||
|
}
|
||||||
|
if (!turnMeaning) {
|
||||||
|
pushReason(reasonCodes, "turn_meaning_ref_missing");
|
||||||
|
}
|
||||||
|
if (!hasGroundingAxis({ turnMeaning, requiredAxes })) {
|
||||||
|
pushReason(reasonCodes, "grounding_axis_missing");
|
||||||
|
}
|
||||||
|
if (allowedPrimitives.length === 0 && proposed.length > 0) {
|
||||||
|
pushReason(reasonCodes, "no_allowed_mcp_primitives_after_runtime_filter");
|
||||||
|
}
|
||||||
|
if (allowedPrimitives.length === 0 && proposed.length === 0) {
|
||||||
|
pushReason(reasonCodes, "mcp_primitives_not_proposed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let planStatus: AssistantMcpDiscoveryPlanStatus = "allowed";
|
||||||
|
if (allowedPrimitives.length === 0 && proposed.length > 0) {
|
||||||
|
planStatus = "blocked";
|
||||||
|
} else if (!semanticDataNeed || !turnMeaning || !hasGroundingAxis({ turnMeaning, requiredAxes })) {
|
||||||
|
planStatus = "needs_clarification";
|
||||||
|
} else if (allowedPrimitives.length === 0) {
|
||||||
|
planStatus = "needs_clarification";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planStatus === "allowed") {
|
||||||
|
pushReason(reasonCodes, "guarded_mcp_discovery_plan_allowed");
|
||||||
|
} else if (planStatus === "blocked") {
|
||||||
|
pushReason(reasonCodes, "guarded_mcp_discovery_plan_blocked");
|
||||||
|
} else {
|
||||||
|
pushReason(reasonCodes, "guarded_mcp_discovery_plan_needs_clarification");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_PLAN_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryPolicy",
|
||||||
|
plan_status: planStatus,
|
||||||
|
semantic_data_need: semanticDataNeed,
|
||||||
|
turn_meaning_ref: turnMeaning,
|
||||||
|
allowed_primitives: allowedPrimitives,
|
||||||
|
rejected_primitives: rejectedPrimitives,
|
||||||
|
required_axes: requiredAxes,
|
||||||
|
execution_budget: {
|
||||||
|
max_probe_count: clampInteger(input.maxProbeCount, DEFAULT_DISCOVERY_BUDGET.max_probe_count, 1, MAX_PROBE_COUNT),
|
||||||
|
max_rows_per_probe: clampInteger(
|
||||||
|
input.maxRowsPerProbe,
|
||||||
|
DEFAULT_DISCOVERY_BUDGET.max_rows_per_probe,
|
||||||
|
1,
|
||||||
|
MAX_ROWS_PER_PROBE
|
||||||
|
)
|
||||||
|
},
|
||||||
|
requires_evidence_gate: true,
|
||||||
|
answer_may_use_raw_model_claims: false,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectProbeLimitations(probeResults: AssistantMcpDiscoveryProbeResult[]): string[] {
|
||||||
|
const limitations: string[] = [];
|
||||||
|
for (const probe of probeResults) {
|
||||||
|
const limitation = toNonEmptyString(probe.limitation);
|
||||||
|
if (limitation && !limitations.includes(limitation)) {
|
||||||
|
limitations.push(limitation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return limitations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function probeRowsMatched(probeResults: AssistantMcpDiscoveryProbeResult[]): number {
|
||||||
|
return probeResults.reduce((sum, probe) => {
|
||||||
|
const rows = Number(probe.rows_matched ?? 0);
|
||||||
|
return sum + (Number.isFinite(rows) && rows > 0 ? rows : 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function probeRowsReceived(probeResults: AssistantMcpDiscoveryProbeResult[]): number {
|
||||||
|
return probeResults.reduce((sum, probe) => {
|
||||||
|
const rows = Number(probe.rows_received ?? 0);
|
||||||
|
return sum + (Number.isFinite(rows) && rows > 0 ? rows : 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProbeBypass(plan: AssistantMcpDiscoveryPlanContract, probeResults: AssistantMcpDiscoveryProbeResult[]): boolean {
|
||||||
|
const allowed = new Set<string>(plan.allowed_primitives);
|
||||||
|
return probeResults.some((probe) => !allowed.has(probe.primitive_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function confidenceReasonFor(status: AssistantMcpDiscoveryEvidenceStatus): string {
|
||||||
|
if (status === "confirmed") {
|
||||||
|
return "confirmed_facts_backed_by_allowed_mcp_probe_rows";
|
||||||
|
}
|
||||||
|
if (status === "inferred_only") {
|
||||||
|
return "only_inferred_facts_available_from_allowed_mcp_probe_rows";
|
||||||
|
}
|
||||||
|
if (status === "blocked") {
|
||||||
|
return "runtime_evidence_gate_blocked_discovery_answer";
|
||||||
|
}
|
||||||
|
return "allowed_mcp_probes_did_not_produce_sufficient_evidence";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
|
input: ResolveAssistantMcpDiscoveryEvidenceInput
|
||||||
|
): AssistantMcpDiscoveryEvidenceContract {
|
||||||
|
const probeResults = Array.isArray(input.probeResults) ? input.probeResults : [];
|
||||||
|
const confirmedFacts = toStringList(input.confirmedFacts);
|
||||||
|
const inferredFacts = toStringList(input.inferredFacts);
|
||||||
|
const unknownFacts = toStringList(input.unknownFacts);
|
||||||
|
const sourceRowsSummary = toNonEmptyString(input.sourceRowsSummary);
|
||||||
|
const queryLimitations = [
|
||||||
|
...toStringList(input.queryLimitations),
|
||||||
|
...collectProbeLimitations(probeResults)
|
||||||
|
].filter((item, index, all) => all.indexOf(item) === index);
|
||||||
|
const reasonCodes: string[] = [...input.plan.reason_codes];
|
||||||
|
const rowsMatched = probeRowsMatched(probeResults);
|
||||||
|
const rowsReceived = probeRowsReceived(probeResults);
|
||||||
|
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||||
|
|
||||||
|
if (bypassDetected) {
|
||||||
|
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||||
|
}
|
||||||
|
if (input.plan.plan_status !== "allowed") {
|
||||||
|
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
||||||
|
}
|
||||||
|
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
|
||||||
|
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
||||||
|
}
|
||||||
|
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||||
|
pushReason(reasonCodes, "source_rows_summary_missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
let evidenceStatus: AssistantMcpDiscoveryEvidenceStatus = "insufficient";
|
||||||
|
let coverageStatus: AssistantMcpDiscoveryCoverageStatus = "blocked";
|
||||||
|
let answerPermission: AssistantMcpDiscoveryAnswerPermission = "checked_sources_only";
|
||||||
|
|
||||||
|
if (bypassDetected || input.plan.plan_status !== "allowed") {
|
||||||
|
evidenceStatus = "blocked";
|
||||||
|
coverageStatus = "blocked";
|
||||||
|
answerPermission = "checked_sources_only";
|
||||||
|
} else if (confirmedFacts.length > 0 && rowsMatched > 0 && sourceRowsSummary) {
|
||||||
|
evidenceStatus = "confirmed";
|
||||||
|
coverageStatus = "full";
|
||||||
|
answerPermission = "confirmed_answer";
|
||||||
|
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
||||||
|
} else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
||||||
|
evidenceStatus = "inferred_only";
|
||||||
|
coverageStatus = "partial";
|
||||||
|
answerPermission = "bounded_inference";
|
||||||
|
pushReason(reasonCodes, "inferred_facts_require_bounded_answer");
|
||||||
|
} else {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_evidence_insufficient");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_EVIDENCE_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryPolicy",
|
||||||
|
evidence_status: evidenceStatus,
|
||||||
|
coverage_status: coverageStatus,
|
||||||
|
answer_permission: answerPermission,
|
||||||
|
confirmed_facts: confirmedFacts,
|
||||||
|
inferred_facts: inferredFacts,
|
||||||
|
unknown_facts: unknownFacts,
|
||||||
|
source_rows_summary: sourceRowsSummary,
|
||||||
|
query_plan: input.plan,
|
||||||
|
query_limitations: queryLimitations,
|
||||||
|
confidence_reason: confidenceReasonFor(evidenceStatus),
|
||||||
|
recommended_next_probe: toNonEmptyString(input.recommendedNextProbe),
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -352,6 +352,39 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const ROOT_EXACT_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
|
export const ROOT_EXACT_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
|
||||||
|
rootExactCapability({
|
||||||
|
capability_id: "documents_drilldown",
|
||||||
|
domainId: "counterparty_documents",
|
||||||
|
intent_ids: ["list_documents_by_counterparty", "list_documents_by_contract"],
|
||||||
|
transitions: ["T1", "T2", "T6", "T7"],
|
||||||
|
requiredAnchors: [],
|
||||||
|
optionalAnchors: ["organization", "date_scope", "counterparty", "contract", "document_type"],
|
||||||
|
resultShape: "counterparty_or_contract_document_list",
|
||||||
|
answerObjectShape: "documents_drilldown_list",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "short_counterparty_retarget", "followup_date_carryover"]
|
||||||
|
}),
|
||||||
|
rootExactCapability({
|
||||||
|
capability_id: "address_customer_revenue_and_payments",
|
||||||
|
domainId: "counterparty_revenue",
|
||||||
|
intent_ids: ["customer_revenue_and_payments"],
|
||||||
|
transitions: ["T1", "T2", "T6", "T7"],
|
||||||
|
requiredAnchors: [],
|
||||||
|
optionalAnchors: ["organization", "date_scope", "counterparty", "contract"],
|
||||||
|
resultShape: "counterparty_revenue_payment_flow",
|
||||||
|
answerObjectShape: "counterparty_revenue_summary",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "short_counterparty_retarget", "answer_top_block_matches_current_user_intent"]
|
||||||
|
}),
|
||||||
|
rootExactCapability({
|
||||||
|
capability_id: "address_supplier_payouts_profile",
|
||||||
|
domainId: "counterparty_payouts",
|
||||||
|
intent_ids: ["supplier_payouts_profile"],
|
||||||
|
transitions: ["T1", "T2", "T6", "T7"],
|
||||||
|
requiredAnchors: [],
|
||||||
|
optionalAnchors: ["organization", "date_scope", "counterparty", "contract"],
|
||||||
|
resultShape: "supplier_payout_payment_flow",
|
||||||
|
answerObjectShape: "supplier_payout_summary",
|
||||||
|
scenarioFamilies: ["canonical", "colloquial", "short_counterparty_retarget"]
|
||||||
|
}),
|
||||||
rootExactCapability({
|
rootExactCapability({
|
||||||
capability_id: "confirmed_payables_as_of_date",
|
capability_id: "confirmed_payables_as_of_date",
|
||||||
domainId: "counterparty_debt",
|
domainId: "counterparty_debt",
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,20 @@ describe("address capability policy", () => {
|
||||||
expect(decision.capability_route_enabled).toBe(true);
|
expect(decision.capability_route_enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps counterparty value-flow intents to exact compute capabilities", () => {
|
||||||
|
const revenue = resolveAddressCapabilityRouteDecision("customer_revenue_and_payments");
|
||||||
|
expect(revenue.capability_id).toBe("address_customer_revenue_and_payments");
|
||||||
|
expect(revenue.capability_layer).toBe("compute");
|
||||||
|
expect(revenue.capability_route_mode).toBe("exact");
|
||||||
|
expect(revenue.capability_route_enabled).toBe(true);
|
||||||
|
expect(revenue.capability_route_reason).toBe("counterparty_value_flow_route_enabled");
|
||||||
|
|
||||||
|
const payouts = resolveAddressCapabilityRouteDecision("supplier_payouts_profile");
|
||||||
|
expect(payouts.capability_id).toBe("address_supplier_payouts_profile");
|
||||||
|
expect(payouts.capability_layer).toBe("compute");
|
||||||
|
expect(payouts.capability_route_mode).toBe("exact");
|
||||||
|
});
|
||||||
|
|
||||||
it("maps heuristic list intents to heuristic compute route mode", () => {
|
it("maps heuristic list intents to heuristic compute route mode", () => {
|
||||||
const decision = resolveAddressCapabilityRouteDecision("list_receivables_counterparties");
|
const decision = resolveAddressCapabilityRouteDecision("list_receivables_counterparties");
|
||||||
expect(decision.capability_id).toBe("receivables_candidates_list");
|
expect(decision.capability_id).toBe("receivables_candidates_list");
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,35 @@ describe("assistant capability runtime binding adapter", () => {
|
||||||
expect(binding.violations).toEqual([]);
|
expect(binding.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("binds counterparty revenue answers to the phase18 exact contract", () => {
|
||||||
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
|
addressDebug: {
|
||||||
|
capability_id: "address_customer_revenue_and_payments",
|
||||||
|
detected_intent: "customer_revenue_and_payments",
|
||||||
|
detected_mode: "address_query",
|
||||||
|
capability_layer: "compute",
|
||||||
|
capability_route_mode: "exact",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
organization: "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
rows_matched: 16,
|
||||||
|
route_expectation_status: "matched"
|
||||||
|
},
|
||||||
|
groundingStatus: "grounded",
|
||||||
|
replyType: "factual"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(binding.binding_status).toBe("bound");
|
||||||
|
expect(binding.binding_action).toBe("allow");
|
||||||
|
expect(binding.capability_contract_id).toBe("address_customer_revenue_and_payments");
|
||||||
|
expect(binding.transition_id).toBe("T1");
|
||||||
|
expect(binding.transition_allowed).toBe(true);
|
||||||
|
expect(binding.result_shape).toBe("counterparty_revenue_payment_flow");
|
||||||
|
expect(binding.provided_anchors).toEqual(expect.arrayContaining(["counterparty", "organization"]));
|
||||||
|
expect(binding.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("blocks selected-object capabilities when required anchors are missing", () => {
|
it("blocks selected-object capabilities when required anchors are missing", () => {
|
||||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
addressDebug: {
|
addressDebug: {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -95,6 +95,20 @@ describe("assistant runtime contract registry", () => {
|
||||||
expect(contract?.execution_adapter).toBe("AddressQueryService");
|
expect(contract?.execution_adapter).toBe("AddressQueryService");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("declares counterparty document and value-flow contracts used by semantic dialog authority replay", () => {
|
||||||
|
const documents = getAssistantCapabilityContractByIntent("list_documents_by_counterparty");
|
||||||
|
expect(documents?.capability_id).toBe("documents_drilldown");
|
||||||
|
expect(documents?.supported_transition_classes).toEqual(["T1", "T2", "T6", "T7"]);
|
||||||
|
expect(documents?.result_shape).toBe("counterparty_or_contract_document_list");
|
||||||
|
|
||||||
|
const revenue = getAssistantCapabilityContractByIntent("customer_revenue_and_payments");
|
||||||
|
expect(revenue?.capability_id).toBe("address_customer_revenue_and_payments");
|
||||||
|
expect(revenue?.runtime_lane).toBe("address_exact");
|
||||||
|
expect(revenue?.requires_focus_object).toBe(false);
|
||||||
|
expect(revenue?.supported_transition_classes).toEqual(["T1", "T2", "T6", "T7"]);
|
||||||
|
expect(revenue?.required_scenario_families).toContain("answer_top_block_matches_current_user_intent");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps truth semantics outside answer wording for every pilot inventory capability", () => {
|
it("keeps truth semantics outside answer wording for every pilot inventory capability", () => {
|
||||||
for (const contract of listInventoryCapabilityContracts()) {
|
for (const contract of listInventoryCapabilityContracts()) {
|
||||||
expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient");
|
expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient");
|
||||||
|
|
@ -130,6 +144,27 @@ describe("assistant runtime contract registry", () => {
|
||||||
expect(decision.carryover_eligibility).toBe("object_only");
|
expect(decision.carryover_eligibility).toBe("object_only");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves phase18 counterparty revenue shadow as a root exact contract", () => {
|
||||||
|
const decision = resolveAssistantRuntimeContractShadow({
|
||||||
|
addressDebug: {
|
||||||
|
capability_id: "address_customer_revenue_and_payments",
|
||||||
|
detected_intent: "customer_revenue_and_payments",
|
||||||
|
detected_mode: "address_query",
|
||||||
|
capability_layer: "compute",
|
||||||
|
capability_route_mode: "exact",
|
||||||
|
rows_matched: 16,
|
||||||
|
route_expectation_status: "matched"
|
||||||
|
},
|
||||||
|
groundingStatus: "grounded"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.transition_contract_id).toBe("T1");
|
||||||
|
expect(decision.capability_contract_id).toBe("address_customer_revenue_and_payments");
|
||||||
|
expect(decision.capability_contract_reason).toEqual(["debug_capability_id_matched_contract"]);
|
||||||
|
expect(decision.truth_gate_contract_status).toBe("full_confirmed");
|
||||||
|
expect(decision.carryover_eligibility).toBe("root_only");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves meta follow-up and blocked route expectation in shadow mode", () => {
|
it("resolves meta follow-up and blocked route expectation in shadow mode", () => {
|
||||||
const metaDecision = resolveAssistantRuntimeContractShadow({
|
const metaDecision = resolveAssistantRuntimeContractShadow({
|
||||||
addressRuntimeMeta: {
|
addressRuntimeMeta: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue