АРЧ АП11 - Commit title: Добавить контрактный слой переходов и capability-деклараций ассистента

This commit is contained in:
2026-04-15 22:53:57 +03:00
parent 8056bdfaf2
commit 93ad18daa3
30 changed files with 4175 additions and 13 deletions
@@ -47,4 +47,21 @@ describe("inventory warehouse anchor extraction", () => {
expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current");
expect(result.semantic_frame?.date_basis_hint).toBe("implicit_current_snapshot");
});
it("does not materialize profanity tail as warehouse anchor in slang stock query", () => {
const filters = extractAddressFilters(
"ассистент, рассказывай что нам на складе ебаном лежит",
"inventory_on_hand_as_of_date"
).extracted_filters;
expect(filters.warehouse).toBeUndefined();
});
it("does not materialize repair phrasing as warehouse anchor in stock follow-up", () => {
const filters = extractAddressFilters(
"остатки на складе какие имелось в виду",
"inventory_on_hand_as_of_date"
).extracted_filters;
expect(filters.warehouse).toBeUndefined();
});
});
@@ -5038,6 +5038,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(result.intent).toBe("inventory_sale_trace_for_item");
});
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
expect(result.intent).toBe("inventory_sale_trace_for_item");
});
it("keeps inventory provenance wording out of inventory-on-hand routing", () => {
const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?");
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
@@ -1064,4 +1064,65 @@ describe("assistant living chat mode", () => {
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
expect(chatClient.chat).toHaveBeenCalledTimes(0);
});
it("does not treat short emotional reaction as organization-selection confirmation", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-chat-affective-reaction",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-affective-reaction";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-selected-org",
session_id: sessionId,
role: "assistant",
text: "Отлично, фиксирую рабочую организацию: ООО Альтернатива Плюс.",
reply_type: "factual_with_explanation",
created_at: new Date().toISOString(),
trace_id: "chat-seed-selected-org",
debug: {
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
assistant_selected_organization: "ООО Альтернатива Плюс",
assistant_active_organization: "ООО Альтернатива Плюс"
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-affective-reaction" },
outputText: "Понимаю, это выглядит неприятно. Давай разберем следующий шаг.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "ну ахуеть",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.living_chat_response_source).not.toBe("deterministic_data_scope_selection_contract");
expect(String(response.assistant_reply)).not.toContain("фиксирую рабочую организацию");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy";
import {
getAssistantCapabilityContract,
getAssistantCapabilityContractByIntent,
getAssistantTransitionContract,
listAssistantTransitionContracts,
listInventoryCapabilityContracts
} from "../src/services/assistantRuntimeContractRegistry";
describe("assistant runtime contract registry", () => {
it("declares the architecture turnaround transition set T1-T10", () => {
const transitions = listAssistantTransitionContracts();
expect(transitions.map((item) => item.transition_id)).toEqual(["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10"]);
const ids = new Set(transitions.map((item) => item.transition_id));
expect(ids.size).toBe(10);
expect(transitions.every((item) => item.schema_version === "assistant_runtime_contracts_v1")).toBe(true);
});
it("keeps selected-object action follow-ups object-scoped instead of generic-chat scoped", () => {
const transition = getAssistantTransitionContract("T4");
expect(transition).not.toBeNull();
expect(transition?.required_prior_state).toContain("selected_object_frame_state");
expect(transition?.allowed_carryover_depth).toBe("object_only");
expect(transition?.forbidden_carryover).toContain("generic_chat_fallback");
expect(transition?.forbidden_carryover).toContain("object_focus_reset");
});
it("declares meta follow-up as answer-object reuse, not blind exact-route replay", () => {
const transition = getAssistantTransitionContract("T8");
expect(transition).not.toBeNull();
expect(transition?.required_prior_state).toEqual(["answer_context_state", "coverage_gate_state"]);
expect(transition?.allowed_carryover_depth).toBe("meta_only");
expect(transition?.state_mutations).toContain("reuse_answer_object_without_blind_replay");
expect(transition?.forbidden_carryover).toContain("blind_exact_route_replay");
expect(transition?.expected_answer_mode).toBe("meta");
});
it("keeps pilot inventory capability ids aligned with the current address capability policy", () => {
for (const contract of listInventoryCapabilityContracts()) {
for (const intent of contract.intent_ids) {
const decision = resolveAddressCapabilityRouteDecision(intent);
expect(decision.capability_id).toBe(contract.capability_id);
expect(decision.capability_route_mode).toBe("exact");
}
}
});
it("declares root inventory snapshot as root-capable and focus-object-free", () => {
const contract = getAssistantCapabilityContract("confirmed_inventory_on_hand_as_of_date");
expect(contract).not.toBeNull();
expect(contract?.entry_modes).toEqual(["root_entry", "root_followup", "clarification_resume"]);
expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T7"]);
expect(contract?.requires_focus_object).toBe(false);
expect(contract?.result_shape).toBe("item_list_with_quantity_cost_warehouse_organization");
expect(contract?.required_scenario_families).toContain("colloquial");
});
it("declares selected-item provenance as focus-object and bundle-aware", () => {
const contract = getAssistantCapabilityContractByIntent("inventory_purchase_provenance_for_item");
expect(contract?.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
expect(contract?.requires_focus_object).toBe(true);
expect(contract?.accepted_focus_object_kinds).toEqual(["inventory_item", "item"]);
expect(contract?.supported_transition_classes).toEqual(["T3", "T4", "T5", "T7"]);
expect(contract?.required_anchors).toEqual(["item"]);
expect(contract?.bundle_reuse_policy).toBe("provenance_bundle_preferred");
expect(contract?.anchor_admissibility_rules).toContain("confirmed_focus_object_beats_semantic_hint");
expect(contract?.required_scenario_families).toContain("ui_selected_object_colloquial");
expect(contract?.required_scenario_families).toContain("pronoun_followup");
});
it("keeps truth semantics outside answer wording for every pilot inventory capability", () => {
for (const contract of listInventoryCapabilityContracts()) {
expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient");
expect(contract.truth_mode_fallbacks).toEqual(["limited", "clarification_required", "unsupported"]);
expect(contract.blocked_reason_codes).toEqual(
expect.arrayContaining(["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"])
);
expect(contract.route_expectation_failure_behavior).toBe("blocked_route_expectation_failure");
expect(contract.execution_error_behavior).toBe("blocked_execution_error");
}
});
});