АРЧ АП11 - Архитектура: вынести inventory intent-family из resolveAddressIntent и зафиксировать pre-expansion cut
This commit is contained in:
parent
29721d16cd
commit
50c66e71a8
|
|
@ -25,9 +25,9 @@ This snapshot is based on:
|
|||
|
||||
Latest graph rebuild:
|
||||
|
||||
- `5261 nodes`
|
||||
- `11347 edges`
|
||||
- `134 communities`
|
||||
- `5284 nodes`
|
||||
- `11402 edges`
|
||||
- `138 communities`
|
||||
|
||||
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
|
||||
|
||||
Estimated overall turnaround completion: `~90%`
|
||||
Estimated overall turnaround completion: `~91%`
|
||||
|
||||
### Phase 0. Shared Baseline
|
||||
|
||||
|
|
@ -163,12 +163,13 @@ Remaining debt:
|
|||
|
||||
### Phase 3. Capability Contracts
|
||||
|
||||
Status: `86%`
|
||||
Status: `88%`
|
||||
|
||||
Reason:
|
||||
|
||||
- critical inventory/address capabilities are materially contract-driven;
|
||||
- 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:
|
||||
|
||||
|
|
@ -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`;
|
||||
- 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 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.
|
||||
|
||||
## What Still Remains The Main Architectural Debt
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.resolveAddressIntent = resolveAddressIntent;
|
||||
const addressInventoryIntentSignals_1 = require("./addressInventoryIntentSignals");
|
||||
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
|
||||
const RECEIVABLES_STRONG = [
|
||||
"кто должен нам",
|
||||
|
|
@ -1544,6 +1545,10 @@ function resolveAddressIntent(userMessage) {
|
|||
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)) {
|
||||
return {
|
||||
intent: "inventory_aging_by_purchase_date",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AddressIntentResolution } from "../types/addressQuery";
|
||||
import { resolveInventoryAddressIntent } from "./addressInventoryIntentSignals";
|
||||
import {
|
||||
hasInventoryProfitabilityCue,
|
||||
hasInventoryPurchaseStem,
|
||||
|
|
@ -1900,6 +1901,11 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
};
|
||||
}
|
||||
|
||||
const inventoryIntent = 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue