Стабилизировать маржинальность номенклатуры

This commit is contained in:
2026-05-22 16:17:07 +03:00
parent f5d86d4bc1
commit 09c6d1aa0e
45 changed files with 2074 additions and 76 deletions
@@ -104,6 +104,15 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.intent).toBe("inventory_aging_by_purchase_date");
});
it("routes nomenclature margin ranking away from OS, bank, and settlement fallbacks", () => {
const result = resolveAddressIntent(
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
);
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
});
it("detects bare historical inventory root with explicit month-year", () => {
const result = resolveAddressIntent("остатки на март 2016");
@@ -104,4 +104,292 @@ describe("inventory profitability selected-object regressions", () => {
expect(reply).toContain("не чистая прибыль компании");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("asks for a period before ranking nomenclature by margin", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
expect(result?.debug.missing_required_filters).toEqual(["period_from", "period_to"]);
const reply = String(result?.reply_text ?? "");
expect(reply).toContain("\u043d\u0443\u0436\u0435\u043d \u043f\u0435\u0440\u0438\u043e\u0434");
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
expect(reply).not.toContain("\u041e\u0421");
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
expect(reply).not.toContain("settlement");
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
});
it("gives a useful accounting limited answer when sales exist but cost is missing", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2020-05-20T00:00:00Z",
Registrator: "Sales document 1",
AccountDt: "62.01",
AccountKt: "41.01",
Amount: 1500,
Quantity: 10,
SubcontoKt1: "Item A",
Organization: "OOO Alternative Plus"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u043c\u0430\u0439 2020"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
const reply = String(result?.reply_text ?? "");
expect(reply).toContain("\u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440\u044b \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
expect(reply).toContain("\u0415\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f");
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
expect(reply).toContain("\u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
expect(reply).toContain("\u0427\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435");
expect(reply).toContain("90.01 / 90.02");
expect(reply).not.toContain("\u041e\u0421");
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("does not offer to show realizations when only cost base exists in the period", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2017-09-12T00:00:00Z",
Registrator: "Purchase document 1",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 700,
Quantity: 2,
SubcontoDt1: "Item C",
Organization: "OOO Alternative Plus"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
const reply = String(result?.reply_text ?? "");
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f \u0431\u0430\u0437\u0430");
expect(reply).toContain("\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0435\u0439 \u0432 \u043f\u0435\u0440\u0438\u043e\u0434\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e");
expect(reply).toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b");
expect(reply).not.toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
expect(reply).not.toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f/\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u0430\u044f");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("keeps cost-base line drilldown inside the nomenclature margin route", () => {
const marginFollowupContext = {
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
previous_filters: {
organization: "OOO Alternative Plus",
period_from: "2017-09-01",
period_to: "2017-09-30"
},
previous_anchor_type: "unknown" as const,
previous_anchor_value: null
};
const result = runAddressDecomposeStage(
"\u043f\u043e\u043a\u0430\u0436\u0438 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b",
marginFollowupContext
);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
expect(result?.filters.missing_required_filters).toEqual([]);
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
});
it("does not pivot margin follow-up account-41 correction into a balance snapshot", () => {
const marginFollowupContext = {
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
previous_filters: {
organization: "OOO Alternative Plus",
period_from: "2017-09-01",
period_to: "2017-09-30"
},
previous_anchor_type: "unknown" as const,
previous_anchor_value: null
};
for (const message of [
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01"
]) {
const result = runAddressDecomposeStage(message, marginFollowupContext);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.intent.intent).not.toBe("account_balance_snapshot");
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
expect(result?.filters.missing_required_filters).toEqual([]);
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
}
});
it("keeps carried period when executing margin account-41 correction", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2017-02-10T00:00:00Z",
Registrator: "Purchase document 2017",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 500,
Quantity: 1,
SubcontoDt1: "Item A",
Organization: "OOO Alternative Plus"
},
{
Period: "2017-03-10T00:00:00Z",
Registrator: "Sales document 2017",
AccountDt: "62.01",
AccountKt: "41.01",
Amount: 900,
Quantity: 1,
SubcontoKt1: "Item A",
Organization: "OOO Alternative Plus"
}
],
rows: [],
error: null
});
const marginFollowupContext = {
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
previous_filters: {
organization: "OOO Alternative Plus",
period_from: "2017-01-01",
period_to: "2017-12-31"
},
previous_anchor_type: "organization" as const,
previous_anchor_value: "OOO Alternative Plus"
};
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
{ followupContext: marginFollowupContext }
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
expect(result?.debug.extracted_filters?.period_from).toBe("2017-01-01");
expect(result?.debug.extracted_filters?.period_to).toBe("2017-12-31");
expect(result?.debug.missing_required_filters).toEqual([]);
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
it("answers period-scoped nomenclature margin ranking with high and low gross-margin buckets", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 4,
matched_rows: 4,
raw_rows: [
{
Period: "2020-01-10T00:00:00Z",
Registrator: "Purchase document 1",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 500,
Quantity: 10,
SubcontoDt1: "Item A",
Organization: "OOO Alternative Plus"
},
{
Period: "2020-02-10T00:00:00Z",
Registrator: "Sales document 1",
AccountDt: "62.01",
AccountKt: "41.01",
Amount: 1500,
Quantity: 10,
SubcontoKt1: "Item A",
Organization: "OOO Alternative Plus"
},
{
Period: "2020-03-10T00:00:00Z",
Registrator: "Purchase document 2",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 1000,
Quantity: 5,
SubcontoDt1: "Item B",
Organization: "OOO Alternative Plus"
},
{
Period: "2020-04-10T00:00:00Z",
Registrator: "Sales document 2",
AccountDt: "62.01",
AccountKt: "41.01",
Amount: 900,
Quantity: 5,
SubcontoKt1: "Item B",
Organization: "OOO Alternative Plus"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 2020 \u0433\u043e\u0434"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
const reply = String(result?.reply_text ?? "");
expect(reply).toContain("\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0432\u0430\u043b\u043e\u0432\u0430\u044f \u043c\u0430\u0440\u0436\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c");
expect(reply).toContain("\u041d\u0438\u0437\u043a\u0430\u044f \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f");
expect(reply).toContain("Item A");
expect(reply).toContain("Item B");
expect(reply).toContain("\u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
expect(reply).not.toContain("\u041e\u0421");
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
});
@@ -499,6 +499,70 @@ describe("assistant address orchestration runtime adapter", () => {
);
});
it("prefers raw margin-ranking account correction over account-balance canonical drift", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
previous_intent: "inventory_margin_ranking_for_nomenclature",
target_intent: "inventory_margin_ranking_for_nomenclature",
root_intent: "inventory_margin_ranking_for_nomenclature",
previous_filters: {
period_from: "2017-01-01",
period_to: "2017-12-31",
organization: "OOO Alternative Plus"
},
previous_anchor_type: "organization",
previous_anchor_value: "OOO Alternative Plus"
}
}));
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
}));
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
schema_version: "address_llm_predecompose_contract_v1",
source_message: sourceMessage,
canonical_message: canonicalMessage,
mode: canonicalMessage === sourceMessage ? "unsupported" : "address_query",
intent: canonicalMessage === sourceMessage ? "unknown" : "account_balance_snapshot"
}));
const rawMessage = "\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01";
const output = await buildAssistantAddressOrchestrationRuntime(
buildInput({
userMessage: rawMessage,
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: true,
effectiveMessage:
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01",
reason: "normalized_fragment_applied",
predecomposeContract: {
mode: "address_query",
intent: "account_balance_snapshot"
}
})),
buildAddressLlmPredecomposeContractV1,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision
})
);
expect(output.addressInputMessage).toBe(rawMessage);
expect(output.addressPreDecompose.applied).toBe(false);
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
expect.objectContaining({
rawUserMessage: rawMessage,
effectiveAddressUserMessage: rawMessage
})
);
});
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
@@ -66,6 +66,35 @@ describe("assistant capability binding response guard", () => {
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
});
it("uses business clarification for nomenclature margin ranking period", () => {
const output = applyAssistantCapabilityBindingResponseGuard({
assistantReply: "unsafe answer",
replyType: "partial_coverage",
capabilityBinding: binding({
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
capability_contract_id: "inventory_inventory_margin_ranking_for_nomenclature",
binding_status: "blocked",
binding_action: "clarify",
required_anchors: ["period"],
provided_anchors: [],
missing_anchors: ["period"],
requires_focus_object: false,
focus_object_binding_status: "not_required",
result_shape: "nomenclature_margin_ranking",
answer_object_shape: "inventory_margin_ranking",
violations: ["required_anchor_missing"],
reason_codes: ["required_anchor_missing"]
})
});
expect(output.replyType).toBe("partial_coverage");
expect(output.assistantReply).toContain("Для рейтинга прибыльности номенклатуры нужен период");
expect(output.assistantReply).toContain("выручку");
expect(output.assistantReply).toContain("себестоимость реализации");
expect(output.assistantReply).not.toContain("period");
expect(output.audit.applied).toBe(true);
});
it("turns blocked incompatible transitions into bounded replies", () => {
const output = applyAssistantCapabilityBindingResponseGuard({
assistantReply: "unsafe answer",
@@ -130,6 +130,34 @@ describe("assistant capability runtime binding adapter", () => {
expect(binding.violations).toEqual([]);
});
it("treats period_from and period_to as satisfying period anchor", () => {
const binding = resolveAssistantCapabilityRuntimeBinding({
addressDebug: {
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
detected_intent: "inventory_margin_ranking_for_nomenclature",
detected_mode: "address_query",
capability_layer: "compute",
capability_route_mode: "exact",
extracted_filters: {
period_from: "2020-05-01",
period_to: "2020-05-31",
organization: "ООО Альтернатива Плюс"
},
rows_matched: 1,
route_expectation_status: "matched"
},
groundingStatus: "grounded",
replyType: "factual"
});
expect(binding.binding_status).toBe("bound");
expect(binding.binding_action).toBe("allow");
expect(binding.required_anchors).toEqual(["period"]);
expect(binding.provided_anchors).toEqual(expect.arrayContaining(["period_from", "period_to"]));
expect(binding.missing_anchors).toEqual([]);
expect(binding.violations).toEqual([]);
});
it("blocks selected-object capabilities when required anchors are missing", () => {
const binding = resolveAssistantCapabilityRuntimeBinding({
addressDebug: {