ARCH: добавить входной адаптер MCP discovery

This commit is contained in:
dctouch 2026-04-20 10:13:32 +03:00
parent 9d6e7066e0
commit 95f3fdafbb
4 changed files with 598 additions and 0 deletions

View File

@ -844,6 +844,29 @@ Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 33/33;
- `npm run build` passed.
## Progress Update - 2026-04-20 MCP Discovery Turn Input Adapter
The eighth implementation slice of Big Block 5 added the adapter between current-turn meaning/predecompose contracts and MCP discovery input:
- `assistantMcpDiscoveryTurnInputAdapter.ts`
- `assistantMcpDiscoveryTurnInputAdapter.test.ts`
This adapter still does not wire discovery into the hot answer path.
It solves the next runtime integration seam:
- maps `assistant_turn_meaning_v1` into `AssistantMcpDiscoveryTurnMeaningRef`;
- extracts counterparty, organization, and date scope from predecompose contracts;
- bootstraps lifecycle/activity-duration questions from raw user wording;
- treats unsupported-but-understood meaning as discovery-eligible instead of stale-replay fallback;
- avoids serializing structured entity candidates as `[object Object]`;
- keeps supported exact routes out of MCP discovery.
Validation:
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 37/37;
- `npm run build` passed.
## Execution Rule
Do not implement this plan as:

View File

@ -0,0 +1,215 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
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 pushUnique(target, value) {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function compactLower(value) {
return String(value ?? "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
}
function candidateValue(value) {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text));
}
function collectEntityCandidates(value) {
const result = [];
if (Array.isArray(value)) {
for (const item of value) {
pushUnique(result, candidateValue(item));
}
return result;
}
pushUnique(result, candidateValue(value));
return result;
}
function collectPredecomposeEntities(predecompose) {
const entities = toRecordObject(predecompose?.entities);
return {
counterparty: toNonEmptyString(entities?.counterparty),
organization: toNonEmptyString(entities?.organization)
};
}
function collectDateScope(predecompose) {
const period = toRecordObject(predecompose?.period);
const asOfDate = toNonEmptyString(period?.as_of_date);
const periodFrom = toNonEmptyString(period?.period_from);
const periodTo = toNonEmptyString(period?.period_to);
if (asOfDate) {
return asOfDate;
}
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
return yearFrom[1];
}
if (periodFrom && periodTo) {
return `${periodFrom}..${periodTo}`;
}
return periodFrom ?? periodTo ?? null;
}
function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
}
function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
if (/(?:document|documents|list_documents)/iu.test(combined)) {
return "document evidence";
}
if (/(?:metadata|schema|catalog)/iu.test(combined)) {
return "1C metadata evidence";
}
return null;
}
function shouldRunDiscovery(input) {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
return false;
}
function buildAssistantMcpDiscoveryTurnInput(input) {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const reasonCodes = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const semanticDataNeed = semanticNeedFor({
domain: rawDomain,
action: rawAction,
unsupported,
lifecycleSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
const turnMeaning = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
};
const cleanTurnMeaning = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
}
if (toNonEmptyString(turnMeaning.asked_action_family)) {
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
}
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
}
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
}
if (turnMeaning.stale_replay_forbidden) {
cleanTurnMeaning.stale_replay_forbidden = true;
}
const runDiscovery = shouldRunDiscovery({
unsupported,
lifecycleSignal,
semanticDataNeed,
explicitIntentCandidate
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal = assistantTurnMeaning
? "assistant_turn_meaning"
: predecomposeContract
? "predecompose_contract"
: lifecycleSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
if (!runDiscovery) {
pushReason(reasonCodes, "mcp_discovery_not_applicable_for_supported_exact_turn");
}
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,278 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
"assistant_mcp_discovery_turn_input_v1" as const;
export type AssistantMcpDiscoveryTurnInputStatus = "ready" | "needs_more_context" | "not_applicable";
export type AssistantMcpDiscoveryTurnInputSource =
| "assistant_turn_meaning"
| "predecompose_contract"
| "raw_text"
| "none";
export interface BuildAssistantMcpDiscoveryTurnInputAdapterInput {
assistantTurnMeaning?: Record<string, unknown> | null;
predecomposeContract?: Record<string, unknown> | null;
userMessage?: string | null;
effectiveMessage?: string | null;
}
export interface AssistantMcpDiscoveryTurnInputContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryTurnInputAdapter";
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
should_run_discovery: boolean;
semantic_data_need: string | null;
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
source_signal: AssistantMcpDiscoveryTurnInputSource;
reason_codes: string[];
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
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 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 pushUnique(target: string[], value: unknown): void {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function compactLower(value: unknown): string {
return String(value ?? "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
}
function candidateValue(value: unknown): string | null {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (
toNonEmptyString(record.value) ??
toNonEmptyString(record.name) ??
toNonEmptyString(record.ref) ??
toNonEmptyString(record.text)
);
}
function collectEntityCandidates(value: unknown): string[] {
const result: string[] = [];
if (Array.isArray(value)) {
for (const item of value) {
pushUnique(result, candidateValue(item));
}
return result;
}
pushUnique(result, candidateValue(value));
return result;
}
function collectPredecomposeEntities(predecompose: Record<string, unknown> | null): {
counterparty: string | null;
organization: string | null;
} {
const entities = toRecordObject(predecompose?.entities);
return {
counterparty: toNonEmptyString(entities?.counterparty),
organization: toNonEmptyString(entities?.organization)
};
}
function collectDateScope(predecompose: Record<string, unknown> | null): string | null {
const period = toRecordObject(predecompose?.period);
const asOfDate = toNonEmptyString(period?.as_of_date);
const periodFrom = toNonEmptyString(period?.period_from);
const periodTo = toNonEmptyString(period?.period_to);
if (asOfDate) {
return asOfDate;
}
const yearFrom = periodFrom?.match(/^(\d{4})-01-01$/);
const yearTo = periodTo?.match(/^(\d{4})-12-31$/);
if (yearFrom && yearTo && yearFrom[1] === yearTo[1]) {
return yearFrom[1];
}
if (periodFrom && periodTo) {
return `${periodFrom}..${periodTo}`;
}
return periodFrom ?? periodTo ?? null;
}
function hasLifecycleSignal(text: string): boolean {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(
text
);
}
function semanticNeedFor(input: {
domain: string | null;
action: string | null;
unsupported: string | null;
lifecycleSignal: boolean;
}): string | null {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) {
return "counterparty lifecycle evidence";
}
if (/(?:turnover|revenue|payment|payout|value)/iu.test(combined)) {
return "counterparty value-flow evidence";
}
if (/(?:document|documents|list_documents)/iu.test(combined)) {
return "document evidence";
}
if (/(?:metadata|schema|catalog)/iu.test(combined)) {
return "1C metadata evidence";
}
return null;
}
function shouldRunDiscovery(input: {
unsupported: string | null;
lifecycleSignal: boolean;
semanticDataNeed: string | null;
explicitIntentCandidate: string | null;
}): boolean {
if (input.lifecycleSignal || input.unsupported) {
return true;
}
if (!input.explicitIntentCandidate && input.semanticDataNeed) {
return true;
}
return false;
}
export function buildAssistantMcpDiscoveryTurnInput(
input: BuildAssistantMcpDiscoveryTurnInputAdapterInput
): AssistantMcpDiscoveryTurnInputContract {
const assistantTurnMeaning = toRecordObject(input.assistantTurnMeaning);
const predecomposeContract = toRecordObject(input.predecomposeContract);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const reasonCodes: string[] = [];
const rawText = compactLower(`${input.userMessage ?? ""} ${input.effectiveMessage ?? ""}`);
const lifecycleSignal = hasLifecycleSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
const rawAction = toNonEmptyString(assistantTurnMeaning?.asked_action_family);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const semanticDataNeed = semanticNeedFor({
domain: rawDomain,
action: rawAction,
unsupported,
lifecycleSignal
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: lifecycleSignal ? "counterparty_lifecycle" : rawDomain,
asked_action_family: lifecycleSignal ? "activity_duration" : rawAction,
explicit_entity_candidates: entityCandidates,
explicit_organization_scope: predecomposeEntities.organization,
explicit_date_scope: collectDateScope(predecomposeContract),
unsupported_but_understood_family: unsupported ?? (lifecycleSignal ? "counterparty_lifecycle" : null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal)
};
const cleanTurnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {};
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
}
if (toNonEmptyString(turnMeaning.asked_action_family)) {
cleanTurnMeaning.asked_action_family = turnMeaning.asked_action_family;
}
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
}
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
}
if (turnMeaning.stale_replay_forbidden) {
cleanTurnMeaning.stale_replay_forbidden = true;
}
const runDiscovery = shouldRunDiscovery({
unsupported,
lifecycleSignal,
semanticDataNeed,
explicitIntentCandidate
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning"
: predecomposeContract
? "predecompose_contract"
: lifecycleSignal
? "raw_text"
: "none";
if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
if (!runDiscovery) {
pushReason(reasonCodes, "mcp_discovery_not_applicable_for_supported_exact_turn");
}
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes
};
}

View File

@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { buildAssistantMcpDiscoveryTurnInput } from "../src/services/assistantMcpDiscoveryTurnInputAdapter";
describe("assistant MCP discovery turn input adapter", () => {
it("maps unsupported assistant turn meaning into a discovery-ready value-flow input", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true,
explicit_entity_candidates: [{ type: "counterparty", value: "SVK", source: "current_turn_loose_entity_tail" }]
},
predecomposeContract: {
entities: { counterparty: "Группа СВК", organization: "Альтернатива" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.turn_meaning_ref?.stale_replay_forbidden).toBe(true);
});
it("bootstraps lifecycle discovery from raw user wording and predecompose scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "Сколько лет мы работаем с Группа СВК?",
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: { period_from: null, period_to: null, as_of_date: null }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.source_signal).toBe("predecompose_contract");
expect(result.semantic_data_need).toBe("counterparty lifecycle evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_lifecycle",
asked_action_family: "activity_duration",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "counterparty_lifecycle",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_lifecycle_signal_detected");
});
it("does not activate discovery for supported exact current-turn intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "list_documents",
explicit_intent_candidate: "list_documents_by_counterparty",
explicit_entity_candidates: [{ value: "SVK" }],
stale_replay_forbidden: false
}
});
expect(result.adapter_status).toBe("not_applicable");
expect(result.should_run_discovery).toBe(false);
expect(result.turn_meaning_ref).toBeNull();
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("never serializes object candidates as [object Object]", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
explicit_entity_candidates: [{ type: "counterparty", value: "SVK" }]
}
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK"]);
expect(result.turn_meaning_ref?.explicit_entity_candidates).not.toContain("[object Object]");
});
});