Inventory breadth: закрепить stock provenance и sale-trace контуры

This commit is contained in:
2026-05-01 11:37:40 +03:00
parent 87c440d6fb
commit b12f370784
34 changed files with 1354 additions and 97 deletions
@@ -24,6 +24,31 @@ describe("address coverage evidence policy", () => {
expect(contract.as_of_date_basis).toBe("explicit_as_of_date");
});
it("keeps relative current snapshot basis even when it materializes to today's ISO date", () => {
const todayIso = new Date().toISOString().slice(0, 10);
const contract = resolveAddressCoverageEvidence({
intent: "inventory_on_hand_as_of_date",
selectedRecipe: "address_inventory_on_hand_as_of_date_v1",
filters: {
as_of_date: todayIso
},
semanticFrame: {
scope_kind: "implicit_self_scope",
anchor_kind: "self_scope",
anchor_value: null,
date_scope_kind: "implicit_current",
date_basis_hint: "implicit_current_snapshot",
self_scope_detected: true,
selected_object_scope_detected: false
},
responseType: "FACTUAL_LIST",
rowsMatched: 1
});
expect(contract.as_of_date_basis).toBe("implicit_current_snapshot");
expect(contract.reason_codes).toContain("as_of_date_basis_implicit_current_snapshot");
});
it("treats factual exact negatives as full confirmed-balance evidence instead of partial noise", () => {
const contract = resolveAddressCoverageEvidence({
intent: "payables_confirmed_as_of_date",
@@ -32,6 +32,13 @@ describe("addressInventoryIntentSignals", () => {
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
});
it("classifies direct Russian purchase-date wording with an explicit item", () => {
const result = resolveAddressIntent("Когда был куплен товар Столешница 600*3050*26 дуб ниагара");
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
expect(result.reasons).toContain("inventory_purchase_date_signal_detected");
});
it("does not steal non-inventory open-items wording into the inventory owner", () => {
const result = resolveInventoryAddressIntent("хвосты покажи по счету 60 на август 2022");
@@ -114,6 +114,52 @@ describe("inventory purchase-date selected-object follow-up", () => {
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
});
it("routes direct canonical purchase-date wording with an explicit item through the exact provenance lane", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2019-02-12T00:00:00Z",
Registrator: "Поступление товаров и услуг 00000000003 от 12.02.2019 0:00:00",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 3724.17,
SubcontoDt1: "Столешница 600*3050*26 дуб ниагара",
SubcontoDt3: "Основной склад",
SubcontoKt1: "Торговый дом \\Союз МСК\\",
SubcontoKt2: "Договор поставки № 12 от 01.02.2019",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("Когда был куплен товар Столешница 600*3050*26 дуб ниагара", {
followupContext: {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
item: "Столешница 600*3050*26 дуб ниагара",
warehouse: "Основной склад",
as_of_date: "2019-03-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
}
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("12.02.2019");
expect(String(result?.reply_text ?? "")).not.toContain("Блок 1");
});
it("routes 'когда примерно мы купили' follow-up to compact purchase-date answer with the carried item", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
@@ -87,14 +87,15 @@ describe("inventory sale trace movement route", () => {
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
expect(query).toContain('ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, ""), 1, 5) = "41.01"');
expect(query).toContain("Движения.СубконтоКт1 В (ВЫБРАТЬ Номенклатура.Ссылка");
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
expect(query).toContain('"41.01" КАК СчетКт');
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
expect(query).toContain(
'Номенклатура.Наименование = "Рабочая станция универсального специалиста (индивидуальное изготовление)"'
);
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
expect(query).not.toContain("2016-06-30");
expect(query).not.toContain("2016-06-01");
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Движения.Организация) = "ООО \\Альтернатива Плюс\\"');
expect(query).not.toContain('ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) = "ООО \\Альтернатива Плюс\\"');
});
});
@@ -71,7 +71,9 @@ describe("inventory sale trace selected-object regressions", () => {
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
});
@@ -133,7 +135,9 @@ describe("inventory sale trace selected-object regressions", () => {
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения");
expect(query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
expect(query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
expect(query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
expect(query).toContain('Номенклатура.Наименование = "Кромка с клеем 33 дуб ниагара 137 м"');
expect(query).not.toContain('Номенклатура.Наименование = "Кромка"');
});
@@ -197,7 +197,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз МСК\\");
});
@@ -246,7 +247,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 альмандин");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-03-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
expect(String(result?.reply_text ?? "")).toContain("Торговый дом \\Союз");
});
@@ -296,7 +298,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-07-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
});
@@ -346,9 +349,10 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30");
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
expect(result?.debug.capability_route_mode).toBe("exact");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
@@ -399,7 +403,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
});
@@ -448,7 +453,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
});
@@ -498,7 +504,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Рабочая станция универсального специалиста (индивидуальное изготовление)");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-05-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
});
@@ -545,7 +552,8 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_documents_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(String(result?.reply_text ?? "")).toContain("Поступление товаров и услуг 00000000077");
});
@@ -646,7 +654,7 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ИП Покупатель");
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
});
@@ -692,7 +700,7 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Пуф арий");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
@@ -740,7 +748,7 @@ describe("inventory selected-object follow-up", () => {
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
expect(result?.debug.selected_recipe).toBe("address_inventory_sale_trace_for_item_v1");
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
});
@@ -787,7 +795,7 @@ describe("inventory selected-object follow-up", () => {
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2019-03-31");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).not.toContain("2019-03-31");
@@ -826,6 +834,164 @@ describe("inventory selected-object follow-up", () => {
expect(String(result?.reply_text ?? "")).not.toContain("совпадений не нашлось");
});
it("keeps semantic stock period for unresolved supplier-link follow-up while querying purchase history up to as-of date", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2020-06-04T00:00:00Z",
Registrator: "Поступление товаров и услуг 00000000012 от 04.06.2020 13:36:29",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 439000,
SubcontoDt1: "Кресло для посетителей Экокожа/хром Цвет - оранжевый",
SubcontoDt3: "Основной склад",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("Какие товары сейчас висят в остатке без понятной привязки к поставщику", {
analysisDateHint: "2021-09-30",
followupContext: {
previous_intent: "inventory_aging_by_purchase_date",
previous_filters: {
as_of_date: "2021-09-30",
organization: "ООО \\Альтернатива Плюс\\"
},
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
period_from: "2021-09-01",
period_to: "2021-09-30",
as_of_date: "2021-09-30",
organization: "ООО \\Альтернатива Плюс\\"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
}
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
expect(result?.debug.extracted_filters?.period_from).toBe("2021-09-01");
expect(result?.debug.extracted_filters?.period_to).toBe("2021-09-30");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-09-30");
expect(result?.debug.reasons).toContain("period_window_semantic_from_inventory_snapshot_context");
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).not.toContain("ДАТАВРЕМЯ(2021, 9, 1");
expect(query).toContain("ДАТАВРЕМЯ(2021, 9, 30, 23, 59, 59)");
});
it("derives semantic stock month for unresolved supplier-link when dialog continuation starts a new topic", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2020-06-04T00:00:00Z",
Registrator: "Purchase document 00000000012 from 2020-06-04",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 439000,
SubcontoDt1: "Office chair",
SubcontoDt3: "Main warehouse",
Organization: "OOO Test Org"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const unresolvedSupplierLinkQuestion =
"\u041a\u0430\u043a\u0438\u0435 \u0442\u043e\u0432\u0430\u0440\u044b \u0441\u0435\u0439\u0447\u0430\u0441 \u0432\u0438\u0441\u044f\u0442 \u0432 \u043e\u0441\u0442\u0430\u0442\u043a\u0435 \u0431\u0435\u0437 \u043f\u043e\u043d\u044f\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043a \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0443";
const result = await service.tryHandle(unresolvedSupplierLinkQuestion, {
analysisDateHint: "2021-09-30",
activeOrganization: "OOO Test Org"
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
expect(result?.debug.extracted_filters?.period_from).toBe("2021-09-01");
expect(result?.debug.extracted_filters?.period_to).toBe("2021-09-30");
expect(result?.debug.extracted_filters?.as_of_date).toBe("2021-09-30");
expect(result?.debug.reasons).toContain("period_window_semantic_from_inventory_as_of_month");
expect(result?.debug.reasons).not.toContain("period_window_semantic_from_inventory_snapshot_context");
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).not.toContain("(2021, 9, 1");
expect(query).toContain("(2021, 9, 30, 23, 59, 59)");
});
it("treats supplier in purchase-to-sale chain as verification party, not stale organization scope", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Период: "2019-12-10T00:00:00Z",
Регистратор: "Поступление товаров и услуг 00000000111 от 10.12.2019 12:00:01",
СчетДт: "41.01",
СчетКт: "",
Сумма: 855000,
Номенклатура: "Шкаф картотечный 1000*400*2100",
Контрагент: "ЭталонМебель",
Организация: "ООО \\Альтернатива Плюс\\",
Количество: 15
},
{
Период: "2020-04-01T00:00:00Z",
Регистратор: "Реализация товаров и услуг 00000000001 от 01.04.2020 0:00:00",
СчетДт: "",
СчетКт: "41.01",
Сумма: 855000,
Номенклатура: "Шкаф картотечный 1000*400*2100",
Контрагент: "ЭталонМебель",
Организация: "ООО \\Альтернатива Плюс\\",
Количество: 15
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
{
activeOrganization: "ООО \\Альтернатива Плюс\\",
llmSemanticHints: {
scope_target_kind: "organization",
scope_target_text: "Гамма-мебель, ООО",
date_scope_kind: "explicit",
self_scope_detected: false,
selected_object_scope_detected: false
},
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
as_of_date: "2020-03-31",
organization: "ООО \\Альтернатива Плюс\\"
},
previous_anchor_type: "organization",
previous_anchor_value: "ООО \\Альтернатива Плюс\\"
}
}
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_to_sale_chain");
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.reasons).toContain("organization_restored_from_inventory_chain_context");
expect(result?.debug.reasons).toContain("inventory_chain_counterparty_anchor_kept_for_verification");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("полностью не подтверждена");
expect(String(result?.reply_text ?? "")).toContain("ЭталонМебель");
});
it.skip("keeps the full selected item when sale trace is asked in canonical wording after provenance", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
@@ -914,9 +1080,10 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.debug.extracted_filters?.item).toBe("Кресло орион");
expect(result?.debug.extracted_filters?.as_of_date).toBeUndefined();
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
expect(result?.debug.extracted_filters?.period_from).toBeUndefined();
expect(result?.debug.extracted_filters?.period_to).toBeUndefined();
expect(result?.debug.reasons).toContain("as_of_date_from_followup_context");
expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
expect(result?.debug.reasons ?? []).not.toContain("as_of_date_cleared_for_history_recovery");
expect(result?.debug.limitations ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date");
@@ -93,4 +93,26 @@ describe("inventory warehouse anchor extraction", () => {
expect(filters.warehouse).toBeUndefined();
});
it("does not materialize current-moment canonical tail as warehouse anchor", () => {
const filters = extractAddressFilters(
"Какие товары находятся на складе в текущий момент",
"inventory_on_hand_as_of_date"
).extracted_filters;
expect(filters.warehouse).toBeUndefined();
});
it("does not split organization-generic stock wording into stale warehouse and counterparty anchors", () => {
const filters = extractAddressFilters(
"Какие товары находились на складе компании в марте 2019 года?",
"inventory_on_hand_as_of_date"
).extracted_filters;
expect(filters.period_from).toBe("2019-03-01");
expect(filters.period_to).toBe("2019-03-31");
expect(filters.as_of_date).toBe("2019-03-31");
expect(filters.warehouse).toBeUndefined();
expect(filters.counterparty).toBeUndefined();
});
});
@@ -157,6 +157,17 @@ describe("address query shape classifier", () => {
expect(filters.warehouse).toBeUndefined();
});
it("extracts supplier anchor from supplier-item-buyer chain without swallowing the arrow tail", () => {
const extraction = extractAddressFilters(
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
"inventory_purchase_to_sale_chain"
);
expect(extraction.extracted_filters.counterparty).toBe("Гамма-мебель, ООО");
expect(extraction.extracted_filters.item).toBe("Шкаф картотечный 1000*400*2100");
expect(extraction.warnings).toContain("supplier_anchor_derived_for_inventory_documentary_chain");
});
it("cuts inventory item anchor before purchase-doc residue tail", () => {
const filters = extractAddressFilters(
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
@@ -317,6 +328,33 @@ describe("address query shape classifier", () => {
expect(plan.query).toContain("41.01");
});
it("builds sale-trace query from sale document rows with explicit buyer fields", () => {
const selected = selectAddressRecipe("inventory_sale_trace_for_item", {
item: "Шкаф картотечный"
});
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_sale_trace_for_item_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
item: "Шкаф картотечный"
});
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
expect(plan.query).toContain("Товары.Номенклатура В (ВЫБРАТЬ Номенклатура.Ссылка");
});
it("builds purchase-to-sale chain query as purchase and sale document union", () => {
const selected = selectAddressRecipe("inventory_purchase_to_sale_chain", {
item: "Шкаф картотечный"
});
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_purchase_to_sale_chain_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
item: "Шкаф картотечный"
});
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары КАК Товары");
expect(plan.query).toContain("ОБЪЕДИНИТЬ ВСЕ");
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
});
it("renders inventory purchase documents from purchase-side 41.01 movements", () => {
const reply = composeFactualReply(
"inventory_purchase_documents_for_item",
@@ -372,7 +410,7 @@ describe("address query shape classifier", () => {
expect(reply.semantics?.balance_confirmed).toBe(true);
});
it("renders inventory sale trace from credit-side 41.01 movements", () => {
it("renders inventory sale trace from sale document rows", () => {
const reply = composeFactualReply(
"inventory_sale_trace_for_item",
[
@@ -400,7 +438,7 @@ describe("address query shape classifier", () => {
expect(reply.text).toContain("Департамент капитального ремонта города Москвы");
});
it("renders purchase-to-sale chain from both sides of 41.01", () => {
it("renders purchase-to-sale chain from purchase and sale document rows", () => {
const reply = composeFactualReply(
"inventory_purchase_to_sale_chain",
[
@@ -437,6 +475,45 @@ describe("address query shape classifier", () => {
expect(reply.text).toContain("Реализация товаров и услуг 0007");
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
});
it("states when requested supplier-to-buyer chain parties are not confirmed by item documents", () => {
const reply = composeFactualReply(
"inventory_purchase_to_sale_chain",
[
{
period: "2019-12-10T00:00:00Z",
registrator: "Поступление товаров и услуг 00000000150 от 10.12.2019 0:00:00",
account_dt: "41.01",
account_kt: "",
amount: 712500,
analytics: ["Шкаф картотечный 1000*400*2100", "ЭталонМебель"],
item: "Шкаф картотечный 1000*400*2100",
counterparty: "ЭталонМебель",
organization: "ООО Альтернатива Плюс"
},
{
period: "2020-04-01T00:00:00Z",
registrator: "Реализация товаров и услуг 00000000001 от 01.04.2020 0:00:00",
account_dt: "",
account_kt: "41.01",
amount: 855000,
analytics: ["Шкаф картотечный 1000*400*2100", "ЭталонМебель"],
item: "Шкаф картотечный 1000*400*2100",
counterparty: "ЭталонМебель",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage:
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы?"
}
);
expect(reply.text.split("\n")[0]).toContain("полностью не подтверждена");
expect(reply.text.split("\n")[0]).toContain("Гамма-мебель, ООО");
expect(reply.text.split("\n")[0]).toContain("Департамент капитального ремонта города Москвы");
expect(reply.text).toContain("ЭталонМебель");
});
});
describe("address compose stage utf8 headers", () => {
@@ -5377,6 +5454,35 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(reply.text).not.toContain("Контур:");
});
it("answers unresolved supplier-link stock questions without asking for a supplier", () => {
const reply = composeFactualReply(
"inventory_supplier_stock_overlap_as_of_date",
[
{
period: "2021-09-30T00:00:00Z",
registrator: "Поступление товаров и услуг 00000000999 от 30.09.2021 0:00:00",
account_dt: "41.01",
account_kt: "60.01",
amount: 1200,
analytics: ["Товар без поставщика", "Основной склад"],
item: "Товар без поставщика",
warehouse: "Основной склад",
organization: 'ООО "Альтернатива Плюс"'
}
],
{
asOfDate: "2021-09-30",
userMessage: "Какие товары сейчас висят в остатке без понятной привязки к поставщику",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text.split("\n")[0]).toContain("без явно выделенного поставщика");
expect(reply.text).toContain("Позиции без явно выделенного поставщика:");
expect(reply.text).not.toContain("Уточните");
});
it("routes inventory provenance questions to a dedicated intent", () => {
const result = resolveAddressIntent("От какого поставщика куплен товар Шкаф картоотечный?");
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
@@ -5416,6 +5522,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(result.intent).toBe("inventory_sale_trace_for_item");
});
it("routes passive buyer wording 'кому был продан товар' to inventory sale trace intent", () => {
const result = resolveAddressIntent("Кому был продан товар Шкаф картотечный 1000*400*2100?");
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");
@@ -43,6 +43,31 @@ describe("assistant capability runtime binding adapter", () => {
);
});
it("allows supplier-overlap inventory aggregate questions without a supplier anchor", () => {
const binding = resolveAssistantCapabilityRuntimeBinding({
addressDebug: {
capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date",
detected_intent: "inventory_supplier_stock_overlap_as_of_date",
detected_mode: "address_query",
capability_layer: "compute",
capability_route_mode: "exact",
extracted_filters: {
warehouse: "Основной склад",
as_of_date: "2021-09-30"
},
rows_matched: 500,
route_expectation_status: "matched"
},
groundingStatus: "grounded",
replyType: "factual"
});
expect(binding.binding_status).toBe("bound");
expect(binding.required_anchors).toEqual([]);
expect(binding.missing_anchors).toEqual([]);
expect(binding.violations).toEqual([]);
});
it("binds selected-object follow-ups through item anchor when focus object is implicit", () => {
const binding = resolveAssistantCapabilityRuntimeBinding({
addressDebug: {
@@ -1008,6 +1008,45 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.semantic_route_arbitration?.strict_deep_investigation_bypass_allowed).toBe(true);
});
it("keeps selected-object purchase-to-sale chain wording in address lane even when LLM predecompose calls it deep", () => {
const question =
'По выбранному объекту "Шкаф картотечный 1000*400*2100": через какие документы прошел путь товара: закупка -> склад -> продажа';
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: question,
effectiveAddressUserMessage: question,
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "deep_analysis",
mode_confidence: "high",
intent: "list_documents_by_counterparty",
intent_confidence: "high"
},
semanticExtractionContract: {
valid: true,
apply_canonical_recommended: true,
reason_codes: ["deep_investigation_signal_detected"],
guard_hints: {
deep_investigation_signal_detected: true
},
extraction: {
query_shape: "DOCUMENT_LIST",
aggregation_profile: "list_lookup"
}
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
expect(decision.orchestrationContract?.address_intent).toBe("inventory_purchase_to_sale_chain");
});
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а на текущую дату",
@@ -135,6 +135,43 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
});
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "inventory_purchase_to_sale_chain",
selected_recipe: "address_inventory_purchase_to_sale_chain_v1",
mcp_call_status: "matched_non_empty",
truth_mode: "confirmed",
capability_binding_status: "bound",
capability_binding_violations: [],
answer_shape_contract: {
reply_type: "factual",
capability_contract_id: "inventory_inventory_purchase_to_sale_chain"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
metadata_scope_hint: "склад"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toContain("цепочка поставки и продажи подтверждена");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
});
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",