АРЧ АП11 - Архитектура: вынести inventory intent-family из resolveAddressIntent и зафиксировать pre-expansion cut

This commit is contained in:
dctouch 2026-04-17 18:59:17 +03:00
parent 29721d16cd
commit 50c66e71a8
7 changed files with 745 additions and 5 deletions

View File

@ -25,9 +25,9 @@ This snapshot is based on:
Latest graph rebuild: Latest graph rebuild:
- `5261 nodes` - `5284 nodes`
- `11347 edges` - `11402 edges`
- `134 communities` - `138 communities`
Most relevant current god nodes for turnaround `11`: Most relevant current god nodes for turnaround `11`:
@ -126,7 +126,7 @@ This is enough to build targeted semantic packs that are not single-domain toy s
## Honest Phase Status ## Honest Phase Status
Estimated overall turnaround completion: `~90%` Estimated overall turnaround completion: `~91%`
### Phase 0. Shared Baseline ### Phase 0. Shared Baseline
@ -163,12 +163,13 @@ Remaining debt:
### Phase 3. Capability Contracts ### Phase 3. Capability Contracts
Status: `86%` Status: `88%`
Reason: Reason:
- critical inventory/address capabilities are materially contract-driven; - critical inventory/address capabilities are materially contract-driven;
- selected-object and root capability behavior is much more explicit than before. - selected-object and root capability behavior is much more explicit than before.
- inventory intent-family now has an explicit owner in `addressInventoryIntentSignals.ts` instead of staying only as inline signal pressure inside `resolveAddressIntent()`.
Remaining debt: Remaining debt:
@ -251,6 +252,7 @@ Compared with the pre-turnaround baseline, the system is now materially better i
- reply formatting and reply-type classification now have an explicit owner outside `composeStage.ts`; - reply formatting and reply-type classification now have an explicit owner outside `composeStage.ts`;
- confirmed-balance and heuristic-candidate reply contracts now have explicit builders instead of repeated inline `semantics` objects in major compose branches; - confirmed-balance and heuristic-candidate reply contracts now have explicit builders instead of repeated inline `semantics` objects in major compose branches;
- inventory factual replies are now owned by a dedicated module rather than embedded directly in the central compose body; - inventory factual replies are now owned by a dedicated module rather than embedded directly in the central compose body;
- inventory intent classification now has a dedicated owner instead of being only an inline segment inside the central address intent resolver;
- architecture regressions can now be localized to route, transition, truth gate, coverage/evidence, boundary, or meta/memory layers. - architecture regressions can now be localized to route, transition, truth gate, coverage/evidence, boundary, or meta/memory layers.
## What Still Remains The Main Architectural Debt ## What Still Remains The Main Architectural Debt

View File

@ -0,0 +1,128 @@
# 09 - Pre-Expansion Cut (2026-04-17)
## Purpose
This note freezes the practical cutoff for turnaround `11` before mass domain expansion.
The goal is not architectural perfection.
The goal is to separate:
- what must be closed before large-scale domain growth;
- what can be consciously deferred without putting expansion quality at risk.
## Current Read
As of `2026-04-17`, the architecture is no longer in the "foundations are unstable" state.
The system already has:
- extracted route / transition / boundary / meta / memory owners;
- explicit truth and coverage/evidence contracts;
- scenario acceptance artifacts;
- live AGENT semantic replay practice;
- materially stronger selected-object and temporal continuity than in the baseline state.
The remaining problem is different now:
- quality risk is concentrated in a small number of central pressure points;
- these pressure points will amplify regressions once many new domains are added.
## Must Close Before Mass Domain Expansion
### 1. Intent concentration in `resolveAddressIntent()`
Why it matters:
- this is still the main domain-intent concentration point in the graph;
- new domain slices will increase route collisions and accidental cross-triggering;
- follow-up-heavy contours will become harder to reason about if raw signal families stay mixed in one resolver body.
What "done enough" means:
- the most stressed signal families are delegated to dedicated owners;
- inventory, counterparty/documents, and high-risk settlement families are no longer all encoded inline in one body;
- targeted regression packs exist for each extracted family.
Current status:
- inventory signal-family is now delegated to `addressInventoryIntentSignals.ts`;
- this reduces ownership pressure, even though the old inline bodies still remain as cleanup debt.
### 2. Answer semantics pressure in `composeFactualReplyBody()`
Why it matters:
- this still controls too much user-facing behavior;
- technical leakage, limitation phrasing, and answer-shape instability can spread into new domains quickly;
- every new domain added on top of a still-heavy compose body increases presentation inconsistency risk.
What "done enough" means:
- the hottest answer families are routed through dedicated builders/presentation owners;
- blocked / limited / humanized fallback semantics are explicit for the most important contours;
- user-facing replies no longer expose internal route/capability/debug jargon on critical business paths.
### 3. Business-first quality guard on hot contours
Why it matters:
- mass expansion will multiply edge cases faster than humans can manually spot them;
- if hot contours still leak technical junk or weak follow-up logic, the problem will scale with every new domain.
What "done enough" means:
- AGENT semantic runs continue to validate mixed business chains;
- core hot contours are checked for direct-answer usefulness, selected-object continuity, temporal honesty, and no technical leakage;
- enablement gaps are treated as contour-extension work, not dismissed as "unsupported".
## Can Be Deferred After Expansion Starts
### 1. Full `assistantService.ts` beautification
This still matters, but it is no longer the primary pre-expansion blocker.
As long as runtime-critical policy ownership is already externalized, some coordinator-local legacy bodies can remain temporarily.
### 2. Full elimination of every residual helper duplicate
If ownership has already moved and regression coverage exists, residual historical helper bodies are cleanup work, not expansion blockers.
They should be removed during later hardening passes, but they do not all need to be gone before domain growth begins.
### 3. Long-tail micro-polish in reply tone
Minor phrasing improvements can be postponed if:
- the business answer is already truthful;
- no technical internals leak to the user;
- answer shape remains useful and stable.
### 4. UI-first acceptance ergonomics
The current script-driven acceptance loop is good enough for pre-expansion gating.
Promoting every replay step into a more polished UI loop can happen later.
## Recommended Final Turnaround Sequence
### Pass 1
- continue extracting the highest-risk signal families out of `resolveAddressIntent()`;
- keep business behavior stable through focused regression packs;
- treat this as the main pre-expansion hardening track.
### Pass 2
- reduce remaining answer-semantics pressure in `composeFactualReplyBody()`;
- harden blocked / limited / humanized response semantics on the hottest business contours;
- confirm with AGENT replay that user-facing answers stay business-first.
## Practical Exit Condition
Turnaround `11` can be considered "ready for domain expansion" when:
- the main route-collision pressure in `resolveAddressIntent()` is materially reduced;
- the hottest user-facing answer families are protected from technical leakage;
- AGENT replay confirms stable business usefulness on the core mixed chains;
- remaining debt is mostly cleanup debt, not architecture debt that can multiply regressions during expansion.

View File

@ -1,6 +1,7 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAddressIntent = resolveAddressIntent; exports.resolveAddressIntent = resolveAddressIntent;
const addressInventoryIntentSignals_1 = require("./addressInventoryIntentSignals");
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers"); const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
const RECEIVABLES_STRONG = [ const RECEIVABLES_STRONG = [
"кто должен нам", "кто должен нам",
@ -1544,6 +1545,10 @@ function resolveAddressIntent(userMessage) {
reasons: ["documents_by_account_drilldown_signal_detected"] reasons: ["documents_by_account_drilldown_signal_detected"]
}; };
} }
const inventoryIntent = (0, addressInventoryIntentSignals_1.resolveInventoryAddressIntent)(text);
if (inventoryIntent) {
return inventoryIntent;
}
if (/(?:старым\s+закупк(?:ам|и|ах)|относится\s+ли\s+.*\s+к\s+старым\s+закупк(?:ам|и|ах)|очень\s+давно|давно\s+куплен|давно\s+приобретен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(text)) { if (/(?:старым\s+закупк(?:ам|и|ах)|относится\s+ли\s+.*\s+к\s+старым\s+закупк(?:ам|и|ах)|очень\s+давно|давно\s+куплен|давно\s+приобретен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(text)) {
return { return {
intent: "inventory_aging_by_purchase_date", intent: "inventory_aging_by_purchase_date",

View File

@ -0,0 +1,232 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveInventoryAddressIntent = resolveInventoryAddressIntent;
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
function hasInventoryAccount41Anchor(text) {
return /(?:СЃС‡[её]С(?:Р°|Рµ|Сѓ)?|счет(?:Р°|Рµ|Сѓ)?)\D{0,12}41(?:[.,]0?1)?/iu.test(text) || /41(?:[.,]0?1)?\D{0,12}(?:СЃС‡[её]С(?:Р°|Рµ|Сѓ)?|счет(?:Р°|Рµ|Сѓ)?)/iu.test(text);
}
function hasInventoryAsOfCue(text) {
return /(?:сейчас|текущ|на\s+дату|по\s+состоянию|срез|на\s+конец|date|as\s+of|current|now|today)/iu.test(text);
}
function hasInventoryOnHandSignal(text) {
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
const hasStockStateCue = /(?:(?:что|ч[еёо])\s+там\s+на\s+склад(?:е|у|ом|ах)|(?:что|ч[еёо]).*происход(?:ит|ило|ящее).*(?:на\s+)?склад(?:е|у|ом|ах)|происход(?:ит|ило|ящее)\s+на\s+склад(?:е|у|ом|ах)|ситуац(?:ия|ии)\s+на\s+склад(?:е|у|ом|ах)|обстановк(?:а|и)\s+на\s+склад(?:е|у|ом|ах)|what(?:'s| is)?\s+(?:there\s+)?(?:on|in)\s+(?:the\s+)?(?:warehouse|stock)|what(?:'s| is)?\s+happening\s+(?:on|in)\s+(?:the\s+)?(?:warehouse|stock))/iu.test(text);
const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
const hasStockLexeme = /(?:склад(?:Рµ|Сѓ|РѕРј|С|РѕРІ)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme && !hasAccount41Anchor) {
return false;
}
if (hasInventoryProvenanceSignalV2(text) ||
hasInventoryPurchaseDocumentsSignalV2(text) ||
hasInventorySaleTraceSignalV2(text) ||
hasInventoryAgingSignal(text) ||
hasInventoryPurchaseToSaleChainSignal(text)) {
return false;
}
const hasGoodsLexeme = /(?:товар(?:С|РѕРІ|РѕРј|Р°|ные)?|номенклатур|материал(?:С|РѕРІ|Р°|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme = /(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|происход(?:ит|ило|ящее)|ситуац(?:ия|ии)|обстановк(?:а|и)|today|now|current|as\s+of)/iu.test(text);
const hasRequestCue = /(?:покажи|показать|выведи|дай|какие|что|ч[еёо]|какой|сколько|проверь|проверить|чекни|check|show|list|which|what)/iu.test(text);
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true;
}
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
}
function hasSelectedObjectInventoryCue(text) {
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(String(text ?? ""));
}
function hasSelectedObjectInventoryProvenanceSignal(text) {
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text);
}
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
const hasPurchaseDocumentsCue = /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text) ||
/(?:(?:по\s+каким|какими)\s+док[а-яё]*[\s\S]{0,80}(?:купил|куплен)|док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(text);
return hasSelectedObjectInventoryCue(text) && hasPurchaseDocumentsCue;
}
function hasSelectedObjectInventorySaleTraceSignal(text) {
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text);
}
function hasSelectedObjectInventoryProfitabilitySignal(text) {
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(text);
}
function hasInventoryProvenanceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) || /кем\s+поставлен/iu.test(text);
const hasPurchaseCue = /(?:куплен(?:С|Р°|Рѕ)?|закупк|происхождени|откуда|РіРґРµ\s+(?:РјС\s+)?купили(?:\s+(?:это|его|товар|позицию))?|РіРґРµ\s+куплено|РєРѕРіРґР°\s+был\s+куплен|РєРѕРіРґР°\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:С|Р°)?|purchase\s+provenance|purchase\s+date)/iu.test(text) || (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(text);
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryPurchaseDateSignal(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
const hasPurchaseDateCue = /(?:РєРѕРіРґР°\s+(?:примерно\s+)?(?:РјС\s+)?купили|РєРѕРіРґР°\s+был\s+куплен|РєРѕРіРґР°\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text) ||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
return hasItemCue && hasPurchaseDateCue;
}
function hasInventoryPurchaseDocumentsSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasPurchaseDocCue = /(?:РїРѕ\s+каким\s+документам\s+был\s+куплен|РїРѕ\s+каким\s+документам\s+куплен|какими\s+документами\s+был\s+куплен|документ(?:ам|С)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text);
return hasItemCue && hasPurchaseDocCue;
}
function hasInventorySaleTraceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text);
const hasTraceCue = /(?:РєРѕРјСѓ\s+(?:РІ\s+итоге\s+)?(?:РјС\s+)?продали|РєРѕРјСѓ\s+был\s+продан|РєСѓРґР°\s+(?:РІ\s+итоге\s+)?(?:РјС\s+)?продали(?:\s+(?:это|его|товар|позицию))?|РєСѓРґР°\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+РєСѓРїРёР»|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+РїСЂРѕС€[её]Р»\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text);
return hasItemCue && hasTraceCue;
}
function hasInventorySupplierStockOverlapSignal(text) {
const hasDirectSingleItemSupplierQuestion = /(?:РѕС\s+какого\s+поставщика\s+куплен\s+(?:товар|номенклатур(?:Р°|Сѓ|С)|позици(?:СЏ|СЋ|Рё))|РѕС\s+РєРѕРіРѕ\s+куплен\s+(?:товар|номенклатур(?:Р°|Сѓ|С)|позици(?:СЏ|СЋ|Рё)))/iu.test(text);
if (hasDirectSingleItemSupplierQuestion) {
return false;
}
const hasSupplierCue = /(?:поставщик|supplier|vendor|РѕС\s+поставщика|Сѓ\s+поставщика)/iu.test(text);
const hasStockCue = /(?:склад|остат(?:ок|ке|ков)|лежат|лежит|сейчас\s+еще|сейчас\s+ещ[её]|на\s+дату|по\s+состоянию\s+на\s+дату|current\s+stock|stock\s+overlap|что\s+сейчас\s+лежит)/iu.test(text);
return hasSupplierCue && hasStockCue;
}
function hasInventoryAgingSignal(text) {
const hasResidueCue = /(?:остат(?:ок|ки)|в\s+остатке|среди\s+текущих\s+остатков|на\s+складе|stock\s+residue|stock\s+balance)/iu.test(text);
const hasAgingCue = /(?:стар(?:ые|СРј|ых)\s+закупк|стары(?:Рј|С…)\s+закупк(?:ам|Рё|ах)|относит(?:СЃСЏ|СЃСЏ\s+ли)?\s+.*\s+Рє\s+старым\s+закупк|закупал(?:РёСЃСЊ|СЃСЏ)\s+очень\s+давно|очень\s+давно|давно\s+куплен|давно\s+приобретен|куплен\s+задолго\s+РґРѕ(?:\s+даты)?|закуплен(?:С|Р°)?\s+давно|приобретен\s+давно|задолго\s+РґРѕ(?:\s+даты)?|возраст\s+остатк|возраст\s+закупк|aged?\s+stock|old\s+purchase|old\s+purchases|old\s+stock|bought\s+long\s+ago|purchased\s+long\s+ago|aging\s+by\s+purchase\s+date|very\s+old\s+stock|very\s+old\s+purchase|old\s+procurement|older\s+purchases|aged\s+items|old\s+goods)/iu.test(text);
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+приобретен|задолго\s+до)/iu.test(text));
}
function hasInventoryPurchaseToSaleChainSignal(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(text) || text.includes("->");
return hasItemCue && hasChainCue;
}
function hasInventorySupplierToBuyerChainSignal(text) {
const hasSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(text);
const hasBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(text);
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasChainCue = /(?:документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*buyer|supplier\s*->\s*item\s*->\s*customer|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|supplier\s+to\s+item\s+to\s+buyer|поставщик\s*->\s*товар\s*->\s*покупател|поставщик\s*->\s*товар\s*->\s*клиент|поставщик\s*->\s*товар\s*->\s*покупатель|поставщик\s+к\s+покупател|поставщик\s+к\s+клиент|поставщик\s+к\s+товару\s+и\s+покупателю)/iu.test(text) || text.includes("->");
return hasSupplierCue && hasBuyerCue && hasItemCue && hasChainCue;
}
function resolveInventoryAddressIntent(text) {
if (/(?:старым\s+закупк(?:ам|и|ах)|относится\s+ли\s+.*\s+к\s+старым\s+закупк(?:ам|и|ах)|очень\s+давно|давно\s+куплен|давно\s+приобретен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(text)) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "high",
reasons: ["inventory_aging_signal_detected_strong"]
};
}
if (hasInventoryAccount41Anchor(text) && hasInventoryAsOfCue(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_account_41_as_of_date_signal_detected"]
};
}
if (/(?:без\s+понятн(?:ой|ого)\s+привязк(?:и|а)\s+к\s+поставщик|без\s+привязк(?:и|а)\s+к\s+поставщик|unresolved\s+supplier\s+link)/iu.test(text)) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "medium",
reasons: ["inventory_unresolved_provenance_signal_detected"]
};
}
if (hasInventorySupplierStockOverlapSignal(text)) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "medium",
reasons: ["inventory_supplier_stock_overlap_signal_detected"]
};
}
if (/(?:supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|поставщик\s+к\s+покупателю|поставщик\s*->\s*товар\s*->\s*покупател|документально\s+подтвержденн\w+\s+цепочк)/iu.test(text) &&
/(?:поставщик|supplier|vendor)/iu.test(text) &&
/(?:покупател|buyer|customer|client)/iu.test(text) &&
/(?:товар|номенклатур|sku|item|product)/iu.test(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "high",
reasons: ["inventory_supplier_to_buyer_chain_signal_detected_strong"]
};
}
if (hasInventoryPurchaseToSaleChainSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_purchase_to_sale_chain_signal_detected"]
};
}
if (hasInventoryAgingSignal(text)) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "medium",
reasons: ["inventory_aging_signal_detected"]
};
}
if (hasSelectedObjectInventoryProvenanceSignal(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_provenance_signal_detected"]
};
}
if (hasInventoryProvenanceSignalV2(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_provenance_signal_detected"]
};
}
if (hasInventoryPurchaseDateSignal(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_purchase_date_signal_detected"]
};
}
if (hasSelectedObjectInventoryPurchaseDocumentsSignal(text)) {
return {
intent: "inventory_purchase_documents_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_purchase_documents_signal_detected"]
};
}
if (hasInventoryPurchaseDocumentsSignalV2(text)) {
return {
intent: "inventory_purchase_documents_for_item",
confidence: "medium",
reasons: ["inventory_purchase_documents_signal_detected"]
};
}
if (hasSelectedObjectInventoryProfitabilitySignal(text)) {
return {
intent: "inventory_profitability_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_profitability_signal_detected"]
};
}
if (hasSelectedObjectInventorySaleTraceSignal(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
};
}
if (/(?:РєРѕРјСѓ\s+(?:РјС\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|РєРѕРјСѓ\s+РІ\s+итоге\s+РјС\s+впарили)/iu.test(text) &&
/(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySupplierToBuyerChainSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_supplier_to_buyer_chain_signal_detected"]
};
}
if (hasInventoryOnHandSignal(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_on_hand_signal_detected"]
};
}
return null;
}

View File

@ -1,4 +1,5 @@
import type { AddressIntentResolution } from "../types/addressQuery"; import type { AddressIntentResolution } from "../types/addressQuery";
import { resolveInventoryAddressIntent } from "./addressInventoryIntentSignals";
import { import {
hasInventoryProfitabilityCue, hasInventoryProfitabilityCue,
hasInventoryPurchaseStem, hasInventoryPurchaseStem,
@ -1900,6 +1901,11 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
}; };
} }
const inventoryIntent = resolveInventoryAddressIntent(text);
if (inventoryIntent) {
return inventoryIntent;
}
if ( if (
/(?:старым\s+закупк(?:ам|и|ах)|относится\s+ли\s+.*\s+к\s+старым\s+закупк(?:ам|и|ах)|очень\s+давно|давно\s+куплен|давно\s+приобретен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test( /(?:старым\s+закупк(?:ам|и|ах)|относится\s+ли\s+.*\s+к\s+старым\s+закупк(?:ам|и|ах)|очень\s+давно|давно\s+куплен|давно\s+приобретен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(
text text

View File

@ -0,0 +1,334 @@
import type { AddressIntentResolution } from "../types/addressQuery";
import {
hasInventoryProfitabilityCue,
hasInventoryPurchaseStem,
hasInventorySaleCue,
hasInventorySupplierCue
} from "./inventoryLifecycleCueHelpers";
function hasInventoryAccount41Anchor(text: string): boolean {
return /(?:СЃС[РµС]С(?:Р°|Рµ|Сѓ)?|СЃСРµС(?:Р°|Рµ|Сѓ)?)\D{0,12}41(?:[.,]0?1)?/iu.test(text) || /41(?:[.,]0?1)?\D{0,12}(?:СЃС[РµС]С(?:Р°|Рµ|Сѓ)?|СЃСРµС(?:Р°|Рµ|Сѓ)?)/iu.test(text);
}
function hasInventoryAsOfCue(text: string): boolean {
return /(?:СЃРµРСас|СекуС|РЅР°\s+РґР°ССѓ|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ|срез|РЅР°\s+РєРѕРЅРµС|date|as\s+of|current|now|today)/iu.test(
text
);
}
function hasInventoryOnHandSignal(text: string): boolean {
const hasColloquialStockSnapshotCue = /(?:ССРѕ|С[РµСРѕ])\s+(?:Сѓ\s+нас\s+)?РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)(?=$|[\s,.;:!?])/iu.test(
text
);
const hasStockStateCue = /(?:(?:ССРѕ|С[РµСРѕ])\s+Сам\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|(?:ССРѕ|С[РµСРѕ]).*РїСЂРѕРёСЃСРѕРґ(?:РёС|ило|СЏСее).*(?:РЅР°\s+)?склад(?:Рµ|Сѓ|РѕРј|Р°С)|РїСЂРѕРёСЃСРѕРґ(?:РёС|ило|СЏСее)\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|СЃРёССѓР°С(?:РёСЏ|РёРё)\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|РѕР±СЃСановк(?:Р°|Рё)\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|what(?:'s| is)?\s+(?:there\s+)?(?:on|in)\s+(?:the\s+)?(?:warehouse|stock)|what(?:'s| is)?\s+happening\s+(?:on|in)\s+(?:the\s+)?(?:warehouse|stock))/iu.test(
text
);
const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
const hasStockLexeme =
/(?:склад(?:Рµ|Сѓ|РѕРј|С|РѕРІ)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme && !hasAccount41Anchor) {
return false;
}
if (
hasInventoryProvenanceSignalV2(text) ||
hasInventoryPurchaseDocumentsSignalV2(text) ||
hasInventorySaleTraceSignalV2(text) ||
hasInventoryAgingSignal(text) ||
hasInventoryPurchaseToSaleChainSignal(text)
) {
return false;
}
const hasGoodsLexeme =
/(?:Совар(?:С|РѕРІ|РѕРј|Р°|РЅСРµ)?|номенклаССѓСЂ|РјР°Сериал(?:С|РѕРІ|Р°|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme =
/(?:леР(?:РёС|Р°С)|есССЊ|СРёСЃР»(?:РёС(?:СЃСЏ|СЃСЊ)|СЏССЃСЏ)|РѕСЃСР°С(?:РѕРє|РєРё)|срез|РЅР°\s+РґР°С|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ|РЅР°\s+РєРѕРЅРµС|РїСЂРѕРёСЃСРѕРґ(?:РёС|ило|СЏСее)|СЃРёССѓР°С(?:РёСЏ|РёРё)|РѕР±СЃСановк(?:Р°|Рё)|today|now|current|as\s+of)/iu.test(
text
);
const hasRequestCue =
/(?:РїРѕРєР°РРё|показаССЊ|РІСведи|РґР°Р|какие|ССРѕ|С[РµСРѕ]|какоР|сколько|проверь|провериССЊ|Секни|check|show|list|which|what)/iu.test(
text
);
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true;
}
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
}
function hasSelectedObjectInventoryCue(text: string): boolean {
return /(?:РїРѕ\s+РІСбранному\s+объекССѓ|РїРѕ\s+РІСбранноР\s+РїРѕР·РёСРёРё|РїРѕ\s+СЌСРѕР\s+РїРѕР·РёСРёРё|РїРѕ\s+СЌСРѕРјСѓ\s+Совару|РїРѕ\s+нему|РїРѕ\s+РЅРµР|РїРѕ\s+РЅРёРј|РїРѕ\s+нему\s+РРµ|РїРѕ\s+РЅРµР\s+РРµ|selected\s+object)/iu.test(
String(text ?? "")
);
}
function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventorySupplierCue(text);
}
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolean {
const hasPurchaseDocumentsCue =
/(?:РїРѕ\s+каким\s+докуменСам\s+(?:СЌСРѕ|его|СЌСРѕС\s+Совар|СЌССѓ\s+РїРѕР·РёСРёСЋ)\s+купили|РїРѕ\s+каким\s+докуменСам\s+(?:Р±СР»\s+)?куплен|какими\s+докуменСами\s+(?:СЌСРѕ|его|СЌСРѕС\s+Совар|СЌССѓ\s+РїРѕР·РёСРёСЋ)\s+купили|какими\s+докуменСами\s+(?:Р±СР»\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
text
) ||
/(?:(?:РїРѕ\s+каким|какими)\s+РґРѕРє[Р°-СЏС]*[\s\S]{0,80}(?:РєСѓРїРёР»|куплен)|РґРѕРє(?:Рё|уменС[Р°-СЏС]*)[\s\S]{0,80}(?:РїРѕ\s+(?:РЅРёРј|РЅРµР|нему|СЌСРѕР\s+РїРѕР·РёСРёРё|СЌСРѕРјСѓ\s+Совару)|операСРё)|(?:РїРѕ\s+(?:РЅРёРј|РЅРµР|нему|СЌСРѕР\s+РїРѕР·РёСРёРё|СЌСРѕРјСѓ\s+Совару))[\s\S]{0,80}РґРѕРє(?:Рё|уменС[Р°-СЏС]*))/iu.test(
text
);
return hasSelectedObjectInventoryCue(text) && hasPurchaseDocumentsCue;
}
function hasSelectedObjectInventorySaleTraceSignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventorySaleCue(text);
}
function hasSelectedObjectInventoryProfitabilitySignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventoryProfitabilityCue(text);
}
function hasInventoryProvenanceSignalV2(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product|РѕСЃСР°С(?:РѕРє|РєРё)|склад)/iu.test(text);
const hasSupplierCue = hasInventorySupplierCue(text) || /кем\s+РїРѕСЃСавлен/iu.test(text);
const hasPurchaseCue =
/(?:куплен(?:С|Р°|Рѕ)?|закупк|РїСЂРѕРёСЃСРѕРдени|РѕСРєСѓРґР°|РіРґРµ\s+(?:РјС\s+)?купили(?:\s+(?:СЌСРѕ|его|Совар|РїРѕР·РёСРёСЋ))?|РіРґРµ\s+куплено|РєРѕРіРґР°\s+Р±СР»\s+куплен|РєРѕРіРґР°\s+куплен|РґР°СР°\s+закупк|РєСРѕ\s+(?:нам\s+)?РїРѕСЃСавил|кем\s+РїРѕСЃСавлен|РїРѕСЃСавлен(?:С|Р°)?|purchase\s+provenance|purchase\s+date)/iu.test(
text
) || hasInventoryPurchaseStem(text);
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryPurchaseDateSignal(text: string): boolean {
const hasItemCue =
/(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
const hasPurchaseDateCue =
/(?:РєРѕРіРґР°\s+(?:примерно\s+)?(?:РјС\s+)?купили|РєРѕРіРґР°\s+Р±СР»\s+куплен|РєРѕРіРґР°\s+куплен|РґР°СР°\s+закупк|purchase\s+date)/iu.test(
text
) ||
/(?:РєРѕРіРґР°\s+Р±СР»(?:Р°|Рё|Рѕ)?\s+закупк\w*|РєРѕРіРґР°\s+закупк\w*)/iu.test(text);
return hasItemCue && hasPurchaseDateCue;
}
function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text);
const hasPurchaseDocCue = /(?:РїРѕ\s+каким\s+докуменСам\s+Р±СР»\s+куплен|РїРѕ\s+каким\s+докуменСам\s+куплен|какими\s+докуменСами\s+Р±СР»\s+куплен|докуменС(?:ам|С)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
text
);
return hasItemCue && hasPurchaseDocCue;
}
function hasInventorySaleTraceSignalV2(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product|РїРѕР·РёСРё(?:СЏ|СЋ|Рё)|РїСЂРѕРґСѓРєСРё(?:СЏ|СЋ|Рё))/iu.test(text);
const hasTraceCue =
/(?:РєРѕРјСѓ\s+(?:РІ\s+РёСРѕРіРµ\s+)?(?:РјС\s+)?продали|РєРѕРјСѓ\s+Р±СР»\s+продан|РєСѓРґР°\s+(?:РІ\s+РёСРѕРіРµ\s+)?(?:РјС\s+)?продали(?:\s+(?:СЌСРѕ|его|Совар|РїРѕР·РёСРёСЋ))?|РєСѓРґР°\s+(?:Р±Сла\s+)?реализована\s+(?:РїРѕР·РёСРёСЏ|номенклаССѓСЂР°|РїСЂРѕРґСѓРєСРёСЏ)|РєСРѕ\s+РєСѓРїРёР»|buyer|sale\s+trace|trace\s+of\s+sale|Серез\s+какие\s+докуменСС\s+РїСЂРѕС[РµС]Р»\s+РїСѓССЊ\s+Совара|закупк.*склад.*РїСЂРѕРґР°Р|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
text
);
return hasItemCue && hasTraceCue;
}
function hasInventorySupplierStockOverlapSignal(text: string): boolean {
const hasDirectSingleItemSupplierQuestion =
/(?:РѕС\s+какого\s+РїРѕСЃСавСРёРєР°\s+куплен\s+(?:Совар|номенклаССѓСЂ(?:Р°|Сѓ|С)|РїРѕР·РёСРё(?:СЏ|СЋ|Рё))|РѕС\s+РєРѕРіРѕ\s+куплен\s+(?:Совар|номенклаССѓСЂ(?:Р°|Сѓ|С)|РїРѕР·РёСРё(?:СЏ|СЋ|Рё)))/iu.test(
text
);
if (hasDirectSingleItemSupplierQuestion) {
return false;
}
const hasSupplierCue = /(?:РїРѕСЃСавСРёРє|supplier|vendor|РѕС\s+РїРѕСЃСавСРёРєР°|Сѓ\s+РїРѕСЃСавСРёРєР°)/iu.test(text);
const hasStockCue = /(?:склад|РѕСЃСР°С(?:РѕРє|РєРµ|РєРѕРІ)|леРР°С|леРРёС|СЃРµРСас\s+РµСРµ|СЃРµРСас\s+РµС[РµС]|РЅР°\s+РґР°ССѓ|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ\s+РЅР°\s+РґР°ССѓ|current\s+stock|stock\s+overlap|ССРѕ\s+СЃРµРСас\s+леРРёС)/iu.test(
text
);
return hasSupplierCue && hasStockCue;
}
function hasInventoryAgingSignal(text: string): boolean {
const hasResidueCue =
/(?:РѕСЃСР°С(?:РѕРє|РєРё)|РІ\s+РѕСЃСР°СРєРµ|среди\s+СекуСРёС\s+РѕСЃСР°СРєРѕРІ|РЅР°\s+складе|stock\s+residue|stock\s+balance)/iu.test(text);
const hasAgingCue =
/(?:СЃСар(?:СРµ|СРј|СС)\s+закупк|СЃСарС(?:Рј|С)\s+закупк(?:ам|Рё|Р°С)|РѕСРЅРѕСЃРёС(?:СЃСЏ|СЃСЏ\s+ли)?\s+.*\s+Рє\s+СЃСарСРј\s+закупк|закупал(?:РёСЃСЊ|СЃСЏ)\s+РѕСень\s+давно|РѕСень\s+давно|давно\s+куплен|давно\s+РїСЂРёРѕР±СЂРµСен|куплен\s+задолго\s+РґРѕ(?:\s+РґР°СС)?|закуплен(?:С|Р°)?\s+давно|РїСЂРёРѕР±СЂРµСен\s+давно|задолго\s+РґРѕ(?:\s+РґР°СС)?|возрасС\s+РѕСЃСР°СРє|возрасС\s+закупк|aged?\s+stock|old\s+purchase|old\s+purchases|old\s+stock|bought\s+long\s+ago|purchased\s+long\s+ago|aging\s+by\s+purchase\s+date|very\s+old\s+stock|very\s+old\s+purchase|old\s+procurement|older\s+purchases|aged\s+items|old\s+goods)/iu.test(
text
);
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+РїСЂРёРѕР±СЂРµСен|задолго\s+РґРѕ)/iu.test(text));
}
function hasInventoryPurchaseToSaleChainSignal(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text);
const hasChainCue =
/(?:закупк.*склад.*РїСЂРѕРґР°Р|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*РїСЂРѕРґР°РР°|СепоСРє[аи]\s+РґРІРёРен|докуменСально\s+РїРѕРґСверРденн\w+\s+СепоСРє|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(
text
) || text.includes("->");
return hasItemCue && hasChainCue;
}
function hasInventorySupplierToBuyerChainSignal(text: string): boolean {
const hasSupplierCue = /(?:РїРѕСЃСавСРёРє|supplier|vendor)/iu.test(text);
const hasBuyerCue = /(?:РїРѕРєСѓРїР°Сел|buyer|customer|client)/iu.test(text);
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text);
const hasChainCue =
/(?:докуменСально\s+РїРѕРґСверРденн\w+\s+СепоСРє|supplier\s*->\s*item\s*->\s*buyer|supplier\s*->\s*item\s*->\s*customer|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|supplier\s+to\s+item\s+to\s+buyer|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*РїРѕРєСѓРїР°Сел|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*клиенС|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*РїРѕРєСѓРїР°Сель|РїРѕСЃСавСРёРє\s+Рє\s+РїРѕРєСѓРїР°Сел|РїРѕСЃСавСРёРє\s+Рє\s+клиенС|РїРѕСЃСавСРёРє\s+Рє\s+Совару\s+Рё\s+РїРѕРєСѓРїР°Селю)/iu.test(
text
) || text.includes("->");
return hasSupplierCue && hasBuyerCue && hasItemCue && hasChainCue;
}
export function resolveInventoryAddressIntent(text: string): AddressIntentResolution | null {
if (
/(?:СЃСарСРј\s+закупк(?:ам|Рё|Р°С)|РѕСРЅРѕСЃРёССЃСЏ\s+ли\s+.*\s+Рє\s+СЃСарСРј\s+закупк(?:ам|Рё|Р°С)|РѕСень\s+давно|давно\s+куплен|давно\s+РїСЂРёРѕР±СЂРµСен|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(
text
)
) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "high",
reasons: ["inventory_aging_signal_detected_strong"]
};
}
if (hasInventoryAccount41Anchor(text) && hasInventoryAsOfCue(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_account_41_as_of_date_signal_detected"]
};
}
if (
/(?:без\s+РїРѕРЅСЏСРЅ(?:РѕР|РѕРіРѕ)\s+РїСЂРёРІСЏР·Рє(?:Рё|Р°)\s+Рє\s+РїРѕСЃСавСРёРє|без\s+РїСЂРёРІСЏР·Рє(?:Рё|Р°)\s+Рє\s+РїРѕСЃСавСРёРє|unresolved\s+supplier\s+link)/iu.test(
text
)
) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "medium",
reasons: ["inventory_unresolved_provenance_signal_detected"]
};
}
if (hasInventorySupplierStockOverlapSignal(text)) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "medium",
reasons: ["inventory_supplier_stock_overlap_signal_detected"]
};
}
if (
/(?:supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|РїРѕСЃСавСРёРє\s+Рє\s+РїРѕРєСѓРїР°Селю|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*РїРѕРєСѓРїР°Сел|докуменСально\s+РїРѕРґСверРденн\w+\s+СепоСРє)/iu.test(
text
) &&
/(?:РїРѕСЃСавСРёРє|supplier|vendor)/iu.test(text) &&
/(?:РїРѕРєСѓРїР°Сел|buyer|customer|client)/iu.test(text) &&
/(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text)
) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "high",
reasons: ["inventory_supplier_to_buyer_chain_signal_detected_strong"]
};
}
if (hasInventoryPurchaseToSaleChainSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_purchase_to_sale_chain_signal_detected"]
};
}
if (hasInventoryAgingSignal(text)) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "medium",
reasons: ["inventory_aging_signal_detected"]
};
}
if (hasSelectedObjectInventoryProvenanceSignal(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_provenance_signal_detected"]
};
}
if (hasInventoryProvenanceSignalV2(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_provenance_signal_detected"]
};
}
if (hasInventoryPurchaseDateSignal(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_purchase_date_signal_detected"]
};
}
if (hasSelectedObjectInventoryPurchaseDocumentsSignal(text)) {
return {
intent: "inventory_purchase_documents_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_purchase_documents_signal_detected"]
};
}
if (hasInventoryPurchaseDocumentsSignalV2(text)) {
return {
intent: "inventory_purchase_documents_for_item",
confidence: "medium",
reasons: ["inventory_purchase_documents_signal_detected"]
};
}
if (hasSelectedObjectInventoryProfitabilitySignal(text)) {
return {
intent: "inventory_profitability_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_profitability_signal_detected"]
};
}
if (hasSelectedObjectInventorySaleTraceSignal(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
};
}
if (
/(?:РєРѕРјСѓ\s+(?:РјС\s+)?впарили(?:\s+(?:СЌСРѕ|его|Совар|РїРѕР·РёСРёСЋ))?|РєРѕРјСѓ\s+РІ\s+РёСРѕРіРµ\s+РјС\s+впарили)/iu.test(text) &&
/(?:Совар|номенклаССѓСЂ|sku|item|product|РїРѕР·РёСРё(?:СЏ|СЋ|Рё)|РїСЂРѕРґСѓРєСРё(?:СЏ|СЋ|Рё))/iu.test(text)
) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySupplierToBuyerChainSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_supplier_to_buyer_chain_signal_detected"]
};
}
if (hasInventoryOnHandSignal(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_on_hand_signal_detected"]
};
}
return null;
}

View File

@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { resolveInventoryAddressIntent } from "../src/services/addressInventoryIntentSignals";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
describe("addressInventoryIntentSignals", () => {
it("classifies warehouse snapshot wording through the extracted inventory owner", () => {
const result = resolveInventoryAddressIntent("show inventory on hand as of 2020-03-15");
expect(result?.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.reasons).toContain("inventory_on_hand_signal_detected");
});
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
const result = resolveInventoryAddressIntent("selected object supplier provenance");
expect(result?.intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.reasons).toContain("inventory_selected_object_provenance_signal_detected");
});
it("keeps the main resolver behavior stable through inventory-owner delegation", () => {
const result = resolveAddressIntent("а по этой позиции когда была закупка?");
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");
expect(result).toBeNull();
});
});