АРЧ АП11 - Архитектура после регресса: Архитектура: сохранить свежую дату при inventory root restore и очистить data-scope ответы от грязных лейблов

This commit is contained in:
2026-04-18 09:12:39 +03:00
parent a5ea9adf53
commit 0431595542
24 changed files with 1030 additions and 58 deletions
@@ -52,4 +52,20 @@ describe("address counterparty utf8 regression", () => {
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("classifies direct company activity-age wording with a colloquial organization anchor", () => {
const result = resolveCounterpartyAddressIntent(
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
utf8Deps
);
expect(result?.intent).toBe("counterparty_activity_lifecycle");
expect(result?.reasons).toContain("counterparty_activity_lifecycle_signal_detected");
});
it("keeps the main resolver in the supported contour for direct company activity-age wording", () => {
const result = resolveAddressIntent("а по Альтернативе Плюс сколько лет активности в базе 1С?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
});
@@ -205,4 +205,37 @@ describe("inventory root frame regressions", () => {
expect(result?.filters.extracted_filters.period_to).toBe("2019-07-31");
expect(result?.filters.extracted_filters.as_of_date).toBe("2019-07-31");
});
it("keeps the freshest previous date when inventory root restore follows a receivables step", () => {
const result = runAddressDecomposeStage("остатки по складу на эту же дату", {
previous_intent: "receivables_confirmed_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"',
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
root_anchor_type: "organization",
root_anchor_value: 'ООО "Альтернатива Плюс"',
root_context_only: true,
current_frame_kind: "inventory_root"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.intent.reasons).toContain("intent_restored_to_inventory_root_frame");
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31");
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
});
});
@@ -3746,6 +3746,29 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("keeps colloquial follow-up activity-age wording in the lifecycle aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("а по Альтернативе Плюс сколько лет активности в базе 1С?", {
activeOrganization: 'ООО "Альтернатива Плюс"'
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("keeps colloquial follow-up activity-age wording grounded to the selected organization", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("а по Альтернативе Плюс сколько лет активности в базе 1С?", {
activeOrganization: 'ООО "Альтернатива Плюс"'
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
expect(result?.debug.mcp_call_status).not.toBe("materialized_but_not_anchor_matched");
});
it("routes debt-longevity wording into receivables lane with factual reply", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -8,7 +8,10 @@ function createPolicy() {
if (value === null || value === undefined) {
return null;
}
const text = String(value ?? "").trim();
const text = String(value ?? "")
.replace(/\\/g, "")
.replace(/([А-Яа-яA-Za-z])"([А-Яа-яA-Za-z])/gu, "$1в$2")
.trim();
return text.length > 0 ? text : null;
},
toNonEmptyString: (value: unknown) => {
@@ -38,6 +41,21 @@ describe("assistantBoundaryPolicy", () => {
expect(reply.toLowerCase()).not.toContain("read-only");
});
it("normalizes noisy organization labels in data-scope reply", () => {
const policy = createPolicy();
const reply = policy.buildAssistantDataScopeContractReply({
status: "resolved",
channel: "default",
organizations: ['ООО \\Альтернати"а Плюс\\', 'ООО \\Лайс"уд\\']
});
expect(reply).toContain('ООО Альтернатива Плюс');
expect(reply).toContain('ООО Лайсвуд');
expect(reply).not.toContain('\\"');
expect(reply).not.toContain("\\");
});
it("strips unexpected CJK fragments from live chat reply", () => {
const policy = createPolicy();
@@ -409,8 +409,13 @@ describe("assistant living chat mode", () => {
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(String(response.assistant_reply).toLowerCase()).toContain("могу помочь");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u0440\u0435\u0436\u0438\u043c\u0435 \u0447\u0442\u0435\u043d\u0438\u044f");
expect(String(response.assistant_reply).toLowerCase()).toContain("\u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u044e 1\u0441");
expect(String(response.assistant_reply)).not.toContain("vat_period_snapshot");
expect(String(response.assistant_reply)).not.toContain("inventory_on_hand_as_of_date");
expect(String(response.assistant_reply)).not.toContain("suggest_safe_next_step");
expect(String(response.assistant_reply)).not.toContain("explain_boundary");
expect(response.debug?.tool_gate_reason).toBe("assistant_capability_query_detected");
expect(chatClient.chat).toHaveBeenCalledTimes(0);
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(0);
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
import {
mergeKnownOrganizations,
normalizeOrganizationScopeSearchText,
normalizeOrganizationScopeValue,
organizationsLikelySameEntity,
resolveOrganizationSelectionFromMessage,
scoreOrganizationMentionInMessage
} from "../src/services/assistantOrganizationMatcher";
@@ -17,8 +19,13 @@ describe("assistant organization matcher", () => {
).toEqual(['ООО "Альтернатива Плюс"', "ООО Лайсвуд"]);
});
it("repairs noisy display labels before exposing them to the user", () => {
expect(normalizeOrganizationScopeValue('ООО \\Альтернати"а Плюс\\')).toBe("ООО Альтернатива Плюс");
expect(normalizeOrganizationScopeValue('ООО \\Лайс"уд\\')).toBe("ООО Лайсвуд");
});
it("matches incomplete or reordered organization mention against live candidates", () => {
const resolved = resolveOrganizationSelectionFromMessage("дай что сегодня на складе в конторе ссыт кот", [
const resolved = resolveOrganizationSelectionFromMessage("дай что сегодня на складе в конторе кот ссыт", [
"ООО КОТ ССЫТ ВО ДВОРЕ",
"ООО Альтернатива Плюс"
]);
@@ -34,4 +41,12 @@ describe("assistant organization matcher", () => {
expect(score).toBeGreaterThanOrEqual(90);
});
it("treats minor live label corruption as the same organization entity", () => {
expect(organizationsLikelySameEntity("Альтернатива Плюс", 'ООО "Альтернати"а Плюс"')).toBe(true);
});
it("does not merge different organizations with only one shared token", () => {
expect(organizationsLikelySameEntity('ООО "Альтернатива Плюс"', 'ООО "Альтернатива Минус"')).toBe(false);
});
});
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy";
import { buildRootScopedCarryoverFiltersForTests } from "../src/services/assistantService";
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
@@ -76,8 +77,20 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
isInventoryRootFrameIntent: (intent: unknown) => String(intent ?? "") === "inventory_on_hand_as_of_date",
findRecentAddressFilterValue: () => null,
hasForeignAccountingPivotOverInventoryMessage: () => false,
buildRootScopedCarryoverFilters: (_previousFilters: Record<string, unknown>, inventoryRootFrame: Record<string, unknown>) => ({
...(inventoryRootFrame?.filters ?? {})
buildRootScopedCarryoverFilters: (
previousFilters: Record<string, unknown>,
inventoryRootFrame: Record<string, unknown>
) => ({
organization:
toNonEmptyString(inventoryRootFrame?.filters?.organization) ?? toNonEmptyString(previousFilters?.organization),
warehouse:
toNonEmptyString(inventoryRootFrame?.filters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse),
as_of_date:
toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(inventoryRootFrame?.filters?.as_of_date),
period_from:
toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(inventoryRootFrame?.filters?.period_from),
period_to:
toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(inventoryRootFrame?.filters?.period_to)
}),
inferDisplayedEntityTypeFromIntent: () => "item",
extractDisplayedAddressEntityCandidates: () => [],
@@ -109,7 +122,7 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.root_context_only).toBe(true);
expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.previous_filters).toEqual({
expect(carryover?.followupContext?.previous_filters).toMatchObject({
as_of_date: "2020-03-31",
organization: 'ООО "Альтернатива Плюс"'
});
@@ -131,7 +144,7 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
expect(carryover?.followupContext?.root_context_only).toBe(true);
expect(carryover?.followupContext?.previous_intent).toBeUndefined();
expect(carryover?.followupContext?.previous_filters).toEqual({
expect(carryover?.followupContext?.previous_filters).toMatchObject({
as_of_date: "2020-03-31",
organization: 'ООО "Альтернатива Плюс"'
});
@@ -169,6 +182,7 @@ describe("assistantTransitionPolicy", () => {
expect(contract.anchor_type).toBe("item");
expect(contract.anchor_value).toBe("Рабочая станция");
});
it("prefers carryover target intent over llm contract drift in continuation contract", () => {
const policy = buildPolicy();
@@ -257,6 +271,7 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.previous_intent).toBe("list_documents_by_counterparty");
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
});
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
@@ -308,4 +323,32 @@ describe("assistantTransitionPolicy", () => {
period_to: "2021-03-31"
});
});
it("prefers the freshest previous date scope over a stale inventory root frame during same-date pivot", () => {
const filters = buildRootScopedCarryoverFiltersForTests(
{
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
},
{
filters: {
organization: 'ООО "Альтернатива Плюс"',
warehouse: "Основной склад",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
}
}
);
expect(filters).toEqual({
organization: 'ООО "Альтернатива Плюс"',
warehouse: "Основной склад",
as_of_date: "2020-03-31",
period_from: "2020-03-01",
period_to: "2020-03-31"
});
});
});