diff --git a/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md index b756332..146504a 100644 --- a/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/08 - current_status_audit_2026-04-17.md @@ -25,9 +25,9 @@ This snapshot is based on: Latest graph rebuild: -- `5284 nodes` -- `11402 edges` -- `138 communities` +- `5287 nodes` +- `11407 edges` +- `135 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: `~91%` +Estimated overall turnaround completion: `~92%` ### Phase 0. Shared Baseline @@ -163,17 +163,19 @@ Remaining debt: ### Phase 3. Capability Contracts -Status: `88%` +Status: `89%` 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()`. +- counterparty / documents / contracts / open-items intent-family now also has an explicit owner in `addressCounterpartyIntentSignals.ts`, even though legacy inline branches still remain as cleanup debt inside the central resolver. Remaining debt: - `resolveAddressIntent()` is still too central; +- extracted owner seams now exist for the hottest inventory and counterparty families, but the legacy inline body still keeps graph pressure high until the final cleanup pass is done; - some business contours outside the most exercised inventory/address scenarios remain less explicit. ### Phase 4. Coverage / Evidence / Truth Gate Isolation @@ -253,6 +255,7 @@ Compared with the pre-turnaround baseline, the system is now materially better i - 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; +- counterparty / contract / documents intent classification now also 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 diff --git a/docs/ARCH/11 - architecture_turnaround/09 - pre_expansion_cut_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/09 - pre_expansion_cut_2026-04-17.md index 8c26d68..54c7ded 100644 --- a/docs/ARCH/11 - architecture_turnaround/09 - pre_expansion_cut_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/09 - pre_expansion_cut_2026-04-17.md @@ -47,6 +47,7 @@ What "done enough" means: Current status: - inventory signal-family is now delegated to `addressInventoryIntentSignals.ts`; +- counterparty / documents / contracts / open-items signal-family is now also delegated to `addressCounterpartyIntentSignals.ts`; - this reduces ownership pressure, even though the old inline bodies still remain as cleanup debt. ### 2. Answer semantics pressure in `composeFactualReplyBody()` diff --git a/llm_normalizer/backend/dist/services/addressCounterpartyIntentSignals.js b/llm_normalizer/backend/dist/services/addressCounterpartyIntentSignals.js new file mode 100644 index 0000000..b76a0af --- /dev/null +++ b/llm_normalizer/backend/dist/services/addressCounterpartyIntentSignals.js @@ -0,0 +1,166 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveCounterpartyAddressIntent = resolveCounterpartyAddressIntent; +function resolveCounterpartyAddressIntent(text, deps) { + if (deps.hasOpenContractsListSignal(text)) { + return { + intent: "open_contracts_confirmed_as_of_date", + confidence: "medium", + reasons: ["open_contract_signal_detected"] + }; + } + if (deps.hasAny(text, deps.openItemsHints) && + !deps.hasCounterpartyDebtLongevitySignal(text) && + !deps.hasInventoryAgingSignal(text) && + !deps.hasInventoryProvenanceSignalV2(text) && + !deps.hasInventoryPurchaseDocumentsSignalV2(text) && + !deps.hasInventorySaleTraceSignalV2(text) && + (/(?:контраг|РґРѕРіРѕРІРѕСЂ|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(text) || + deps.hasAccountNumberAnchor(text) || + deps.hasCompactAccountCodeToken(text))) { + return { + intent: "open_items_by_counterparty_or_contract", + confidence: "medium", + reasons: ["open_items_signal_detected"] + }; + } + if (deps.hasPeriodCoverageProfileSignal(text) && + !deps.hasPartyAnchorMention(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text)) { + return { + intent: "period_coverage_profile", + confidence: "high", + reasons: ["period_coverage_profile_signal_detected"] + }; + } + if (deps.hasDocumentTypeAndAccountSectionProfileSignal(text) && + !deps.hasPartyAnchorMention(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text)) { + return { + intent: "document_type_and_account_section_profile", + confidence: "high", + reasons: ["document_type_and_account_section_profile_signal_detected"] + }; + } + if (deps.hasCounterpartyPopulationAndRolesSignal(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text)) { + return { + intent: "counterparty_population_and_roles", + confidence: "high", + reasons: ["counterparty_population_and_roles_signal_detected"] + }; + } + if (deps.hasCounterpartyActivityLifecycleSignal(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text)) { + return { + intent: "counterparty_activity_lifecycle", + confidence: "high", + reasons: ["counterparty_activity_lifecycle_signal_detected"] + }; + } + if (deps.hasContractUsageOverviewSignal(text) && + !deps.hasAccountBalanceSignal(text) && + !deps.hasOpenContractsListSignal(text)) { + return { + intent: "contract_usage_overview", + confidence: "high", + reasons: ["contract_usage_overview_signal_detected"] + }; + } + if (deps.hasCustomerRevenueAndPaymentsSignal(text) && !deps.hasAccountBalanceSignal(text)) { + return { + intent: "customer_revenue_and_payments", + confidence: "high", + reasons: ["customer_revenue_and_payments_signal_detected"] + }; + } + if (deps.hasSupplierPayoutsProfileSignal(text) && !deps.hasAccountBalanceSignal(text)) { + return { + intent: "supplier_payouts_profile", + confidence: "high", + reasons: ["supplier_payouts_profile_signal_detected"] + }; + } + if (deps.hasContractUsageAndValueSignal(text) && + !deps.hasAccountBalanceSignal(text) && + !deps.hasOpenContractsListSignal(text)) { + return { + intent: "contract_usage_and_value", + confidence: "high", + reasons: ["contract_usage_and_value_signal_detected"] + }; + } + if (deps.hasContractListByCounterpartySignal(text)) { + return { + intent: "list_contracts_by_counterparty", + confidence: "medium", + reasons: ["contracts_by_counterparty_signal_detected"] + }; + } + if (deps.hasContractAnchorSignal(text) && deps.hasBankOperationSignal(text)) { + return { + intent: "bank_operations_by_contract", + confidence: "medium", + reasons: ["bank_ops_by_contract_signal_detected"] + }; + } + if (deps.hasContractAnchorSignal(text) && + (deps.hasAny(text, deps.documentsByContractHints) || deps.hasDocumentSignal(text))) { + return { + intent: "list_documents_by_contract", + confidence: "medium", + reasons: ["documents_by_contract_signal_detected"] + }; + } + if (deps.hasAny(text, deps.bankOperationsByCounterpartyHints) && + (deps.hasPartyAnchorMention(text) || deps.hasLooseByAnchorMention(text) || deps.hasHeuristicCounterpartyAnchor(text))) { + return { + intent: "bank_operations_by_counterparty", + confidence: "medium", + reasons: ["bank_ops_by_counterparty_signal_detected"] + }; + } + if ((deps.hasAny(text, deps.documentsByCounterpartyHints) || deps.hasCounterpartyShipmentItemFlowSignal(text)) && + (deps.hasPartyAnchorMention(text) || + deps.hasLooseByAnchorMention(text) || + deps.hasImplicitCounterpartyAnchorAroundDocs(text) || + deps.hasHeuristicCounterpartyAnchor(text) || + deps.hasCounterpartyShipmentItemFlowSignal(text))) { + return { + intent: "list_documents_by_counterparty", + confidence: "medium", + reasons: [ + deps.hasCounterpartyShipmentItemFlowSignal(text) + ? "counterparty_item_flow_signal_detected" + : "documents_by_counterparty_signal_detected" + ] + }; + } + if (deps.hasAccountBalanceSignal(text)) { + return { + intent: "account_balance_snapshot", + confidence: "high", + reasons: ["account_balance_signal_detected"] + }; + } + if (deps.hasLooseByAnchorMention(text) && deps.hasGenericAddressLookupSignal(text)) { + return { + intent: "list_documents_by_counterparty", + confidence: "low", + reasons: ["generic_lookup_with_loose_anchor_fallback"] + }; + } + if (deps.hasAny(text, deps.openContractsHints) && + (text.includes("РґРѕРіРѕРІРѕСЂ") || text.includes("контракт") || text.includes("contract"))) { + return { + intent: "open_contracts_confirmed_as_of_date", + confidence: "medium", + reasons: ["open_contract_signal_detected"] + }; + } + return null; +} diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index aa57fcd..def6a44 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveAddressIntent = resolveAddressIntent; +const addressCounterpartyIntentSignals_1 = require("./addressCounterpartyIntentSignals"); const addressInventoryIntentSignals_1 = require("./addressInventoryIntentSignals"); const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers"); const RECEIVABLES_STRONG = [ @@ -1679,6 +1680,44 @@ function resolveAddressIntent(userMessage) { reasons: ["inventory_on_hand_signal_detected"] }; } + const counterpartyIntent = (0, addressCounterpartyIntentSignals_1.resolveCounterpartyAddressIntent)(text, { + hasAny, + openItemsHints: OPEN_ITEMS_HINTS, + openContractsHints: OPEN_CONTRACTS_HINTS, + documentsByCounterpartyHints: DOCUMENTS_BY_COUNTERPARTY_HINTS, + bankOperationsByCounterpartyHints: BANK_OPERATIONS_BY_COUNTERPARTY_HINTS, + documentsByContractHints: DOCUMENTS_BY_CONTRACT_HINTS, + hasCounterpartyDebtLongevitySignal, + hasInventoryAgingSignal, + hasInventoryProvenanceSignalV2, + hasInventoryPurchaseDocumentsSignalV2, + hasInventorySaleTraceSignalV2, + hasAccountNumberAnchor, + hasCompactAccountCodeToken, + hasPeriodCoverageProfileSignal, + hasPartyAnchorMention, + hasContractAnchorSignal, + hasAccountBalanceSignal, + hasDocumentTypeAndAccountSectionProfileSignal, + hasCounterpartyPopulationAndRolesSignal, + hasCounterpartyActivityLifecycleSignal, + hasContractUsageOverviewSignal, + hasOpenContractsListSignal, + hasCustomerRevenueAndPaymentsSignal, + hasSupplierPayoutsProfileSignal, + hasContractUsageAndValueSignal, + hasContractListByCounterpartySignal, + hasBankOperationSignal, + hasDocumentSignal, + hasLooseByAnchorMention, + hasHeuristicCounterpartyAnchor, + hasCounterpartyShipmentItemFlowSignal, + hasImplicitCounterpartyAnchorAroundDocs, + hasGenericAddressLookupSignal + }); + if (counterpartyIntent) { + return counterpartyIntent; + } if (hasOpenContractsListSignal(text)) { return { intent: "open_contracts_confirmed_as_of_date", diff --git a/llm_normalizer/backend/src/services/addressCounterpartyIntentSignals.ts b/llm_normalizer/backend/src/services/addressCounterpartyIntentSignals.ts new file mode 100644 index 0000000..d0f98b9 --- /dev/null +++ b/llm_normalizer/backend/src/services/addressCounterpartyIntentSignals.ts @@ -0,0 +1,250 @@ +import type { AddressIntentResolution } from "../types/addressQuery"; + +type HintList = string[]; + +type CounterpartyIntentDeps = { + hasAny: (text: string, hints: string[]) => boolean; + openItemsHints: HintList; + openContractsHints: HintList; + documentsByCounterpartyHints: HintList; + bankOperationsByCounterpartyHints: HintList; + documentsByContractHints: HintList; + hasCounterpartyDebtLongevitySignal: (text: string) => boolean; + hasInventoryAgingSignal: (text: string) => boolean; + hasInventoryProvenanceSignalV2: (text: string) => boolean; + hasInventoryPurchaseDocumentsSignalV2: (text: string) => boolean; + hasInventorySaleTraceSignalV2: (text: string) => boolean; + hasAccountNumberAnchor: (text: string) => boolean; + hasCompactAccountCodeToken: (text: string) => boolean; + hasPeriodCoverageProfileSignal: (text: string) => boolean; + hasPartyAnchorMention: (text: string) => boolean; + hasContractAnchorSignal: (text: string) => boolean; + hasAccountBalanceSignal: (text: string) => boolean; + hasDocumentTypeAndAccountSectionProfileSignal: (text: string) => boolean; + hasCounterpartyPopulationAndRolesSignal: (text: string) => boolean; + hasCounterpartyActivityLifecycleSignal: (text: string) => boolean; + hasContractUsageOverviewSignal: (text: string) => boolean; + hasOpenContractsListSignal: (text: string) => boolean; + hasCustomerRevenueAndPaymentsSignal: (text: string) => boolean; + hasSupplierPayoutsProfileSignal: (text: string) => boolean; + hasContractUsageAndValueSignal: (text: string) => boolean; + hasContractListByCounterpartySignal: (text: string) => boolean; + hasBankOperationSignal: (text: string) => boolean; + hasDocumentSignal: (text: string) => boolean; + hasLooseByAnchorMention: (text: string) => boolean; + hasHeuristicCounterpartyAnchor: (text: string) => boolean; + hasCounterpartyShipmentItemFlowSignal: (text: string) => boolean; + hasImplicitCounterpartyAnchorAroundDocs: (text: string) => boolean; + hasGenericAddressLookupSignal: (text: string) => boolean; +}; + +export function resolveCounterpartyAddressIntent( + text: string, + deps: CounterpartyIntentDeps +): AddressIntentResolution | null { + if (deps.hasOpenContractsListSignal(text)) { + return { + intent: "open_contracts_confirmed_as_of_date", + confidence: "medium", + reasons: ["open_contract_signal_detected"] + }; + } + + if ( + deps.hasAny(text, deps.openItemsHints) && + !deps.hasCounterpartyDebtLongevitySignal(text) && + !deps.hasInventoryAgingSignal(text) && + !deps.hasInventoryProvenanceSignalV2(text) && + !deps.hasInventoryPurchaseDocumentsSignalV2(text) && + !deps.hasInventorySaleTraceSignalV2(text) && + ( + /(?:контраг|РґРѕРіРѕРІРѕСЂ|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test( + text + ) || + deps.hasAccountNumberAnchor(text) || + deps.hasCompactAccountCodeToken(text) + ) + ) { + return { + intent: "open_items_by_counterparty_or_contract", + confidence: "medium", + reasons: ["open_items_signal_detected"] + }; + } + + if ( + deps.hasPeriodCoverageProfileSignal(text) && + !deps.hasPartyAnchorMention(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text) + ) { + return { + intent: "period_coverage_profile", + confidence: "high", + reasons: ["period_coverage_profile_signal_detected"] + }; + } + + if ( + deps.hasDocumentTypeAndAccountSectionProfileSignal(text) && + !deps.hasPartyAnchorMention(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text) + ) { + return { + intent: "document_type_and_account_section_profile", + confidence: "high", + reasons: ["document_type_and_account_section_profile_signal_detected"] + }; + } + + if ( + deps.hasCounterpartyPopulationAndRolesSignal(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text) + ) { + return { + intent: "counterparty_population_and_roles", + confidence: "high", + reasons: ["counterparty_population_and_roles_signal_detected"] + }; + } + + if ( + deps.hasCounterpartyActivityLifecycleSignal(text) && + !deps.hasContractAnchorSignal(text) && + !deps.hasAccountBalanceSignal(text) + ) { + return { + intent: "counterparty_activity_lifecycle", + confidence: "high", + reasons: ["counterparty_activity_lifecycle_signal_detected"] + }; + } + + if ( + deps.hasContractUsageOverviewSignal(text) && + !deps.hasAccountBalanceSignal(text) && + !deps.hasOpenContractsListSignal(text) + ) { + return { + intent: "contract_usage_overview", + confidence: "high", + reasons: ["contract_usage_overview_signal_detected"] + }; + } + + if (deps.hasCustomerRevenueAndPaymentsSignal(text) && !deps.hasAccountBalanceSignal(text)) { + return { + intent: "customer_revenue_and_payments", + confidence: "high", + reasons: ["customer_revenue_and_payments_signal_detected"] + }; + } + + if (deps.hasSupplierPayoutsProfileSignal(text) && !deps.hasAccountBalanceSignal(text)) { + return { + intent: "supplier_payouts_profile", + confidence: "high", + reasons: ["supplier_payouts_profile_signal_detected"] + }; + } + + if ( + deps.hasContractUsageAndValueSignal(text) && + !deps.hasAccountBalanceSignal(text) && + !deps.hasOpenContractsListSignal(text) + ) { + return { + intent: "contract_usage_and_value", + confidence: "high", + reasons: ["contract_usage_and_value_signal_detected"] + }; + } + + if (deps.hasContractListByCounterpartySignal(text)) { + return { + intent: "list_contracts_by_counterparty", + confidence: "medium", + reasons: ["contracts_by_counterparty_signal_detected"] + }; + } + + if (deps.hasContractAnchorSignal(text) && deps.hasBankOperationSignal(text)) { + return { + intent: "bank_operations_by_contract", + confidence: "medium", + reasons: ["bank_ops_by_contract_signal_detected"] + }; + } + + if ( + deps.hasContractAnchorSignal(text) && + (deps.hasAny(text, deps.documentsByContractHints) || deps.hasDocumentSignal(text)) + ) { + return { + intent: "list_documents_by_contract", + confidence: "medium", + reasons: ["documents_by_contract_signal_detected"] + }; + } + + if ( + deps.hasAny(text, deps.bankOperationsByCounterpartyHints) && + (deps.hasPartyAnchorMention(text) || deps.hasLooseByAnchorMention(text) || deps.hasHeuristicCounterpartyAnchor(text)) + ) { + return { + intent: "bank_operations_by_counterparty", + confidence: "medium", + reasons: ["bank_ops_by_counterparty_signal_detected"] + }; + } + + if ( + (deps.hasAny(text, deps.documentsByCounterpartyHints) || deps.hasCounterpartyShipmentItemFlowSignal(text)) && + (deps.hasPartyAnchorMention(text) || + deps.hasLooseByAnchorMention(text) || + deps.hasImplicitCounterpartyAnchorAroundDocs(text) || + deps.hasHeuristicCounterpartyAnchor(text) || + deps.hasCounterpartyShipmentItemFlowSignal(text)) + ) { + return { + intent: "list_documents_by_counterparty", + confidence: "medium", + reasons: [ + deps.hasCounterpartyShipmentItemFlowSignal(text) + ? "counterparty_item_flow_signal_detected" + : "documents_by_counterparty_signal_detected" + ] + }; + } + + if (deps.hasAccountBalanceSignal(text)) { + return { + intent: "account_balance_snapshot", + confidence: "high", + reasons: ["account_balance_signal_detected"] + }; + } + + if (deps.hasLooseByAnchorMention(text) && deps.hasGenericAddressLookupSignal(text)) { + return { + intent: "list_documents_by_counterparty", + confidence: "low", + reasons: ["generic_lookup_with_loose_anchor_fallback"] + }; + } + + if ( + deps.hasAny(text, deps.openContractsHints) && + (text.includes("РґРѕРіРѕРІРѕСЂ") || text.includes("контракт") || text.includes("contract")) + ) { + return { + intent: "open_contracts_confirmed_as_of_date", + confidence: "medium", + reasons: ["open_contract_signal_detected"] + }; + } + + return null; +} diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 69e04f2..c667114 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -1,4 +1,5 @@ import type { AddressIntentResolution } from "../types/addressQuery"; +import { resolveCounterpartyAddressIntent } from "./addressCounterpartyIntentSignals"; import { resolveInventoryAddressIntent } from "./addressInventoryIntentSignals"; import { hasInventoryProfitabilityCue, @@ -2068,6 +2069,45 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti }; } + const counterpartyIntent = resolveCounterpartyAddressIntent(text, { + hasAny, + openItemsHints: OPEN_ITEMS_HINTS, + openContractsHints: OPEN_CONTRACTS_HINTS, + documentsByCounterpartyHints: DOCUMENTS_BY_COUNTERPARTY_HINTS, + bankOperationsByCounterpartyHints: BANK_OPERATIONS_BY_COUNTERPARTY_HINTS, + documentsByContractHints: DOCUMENTS_BY_CONTRACT_HINTS, + hasCounterpartyDebtLongevitySignal, + hasInventoryAgingSignal, + hasInventoryProvenanceSignalV2, + hasInventoryPurchaseDocumentsSignalV2, + hasInventorySaleTraceSignalV2, + hasAccountNumberAnchor, + hasCompactAccountCodeToken, + hasPeriodCoverageProfileSignal, + hasPartyAnchorMention, + hasContractAnchorSignal, + hasAccountBalanceSignal, + hasDocumentTypeAndAccountSectionProfileSignal, + hasCounterpartyPopulationAndRolesSignal, + hasCounterpartyActivityLifecycleSignal, + hasContractUsageOverviewSignal, + hasOpenContractsListSignal, + hasCustomerRevenueAndPaymentsSignal, + hasSupplierPayoutsProfileSignal, + hasContractUsageAndValueSignal, + hasContractListByCounterpartySignal, + hasBankOperationSignal, + hasDocumentSignal, + hasLooseByAnchorMention, + hasHeuristicCounterpartyAnchor, + hasCounterpartyShipmentItemFlowSignal, + hasImplicitCounterpartyAnchorAroundDocs, + hasGenericAddressLookupSignal + }); + if (counterpartyIntent) { + return counterpartyIntent; + } + if (hasOpenContractsListSignal(text)) { return { intent: "open_contracts_confirmed_as_of_date", diff --git a/llm_normalizer/backend/tests/addressCounterpartyIntentSignals.test.ts b/llm_normalizer/backend/tests/addressCounterpartyIntentSignals.test.ts new file mode 100644 index 0000000..f334561 --- /dev/null +++ b/llm_normalizer/backend/tests/addressCounterpartyIntentSignals.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; + +import { resolveCounterpartyAddressIntent } from "../src/services/addressCounterpartyIntentSignals"; +import { resolveAddressIntent } from "../src/services/addressIntentResolver"; + +const defaultDeps = { + hasAny: (text: string, hints: readonly string[]) => hints.some((hint) => text.includes(hint)), + openItemsHints: ["хвост", "долг", "open items"], + openContractsHints: ["договор", "контракт", "open contracts"], + documentsByCounterpartyHints: ["документы по", "documents by counterparty"], + bankOperationsByCounterpartyHints: ["банк", "выписка", "bank operations by counterparty"], + documentsByContractHints: ["документы по договору", "documents by contract"], + hasCounterpartyDebtLongevitySignal: () => false, + hasInventoryAgingSignal: () => false, + hasInventoryProvenanceSignalV2: () => false, + hasInventoryPurchaseDocumentsSignalV2: () => false, + hasInventorySaleTraceSignalV2: () => false, + hasAccountNumberAnchor: (text: string) => /60/.test(text), + hasCompactAccountCodeToken: () => false, + hasPeriodCoverageProfileSignal: (text: string) => text.includes("year coverage"), + hasPartyAnchorMention: (text: string) => text.includes("чепурнов"), + hasContractAnchorSignal: (text: string) => text.includes("договор"), + hasAccountBalanceSignal: (text: string) => text.includes("balance by account"), + hasDocumentTypeAndAccountSectionProfileSignal: (text: string) => text.includes("document profile"), + hasCounterpartyPopulationAndRolesSignal: (text: string) => text.includes("population and roles"), + hasCounterpartyActivityLifecycleSignal: (text: string) => text.includes("activity lifecycle"), + hasContractUsageOverviewSignal: (text: string) => text.includes("usage overview"), + hasOpenContractsListSignal: (text: string) => text.includes("open contracts"), + hasCustomerRevenueAndPaymentsSignal: (text: string) => text.includes("revenue and payments"), + hasSupplierPayoutsProfileSignal: (text: string) => text.includes("supplier payouts"), + hasContractUsageAndValueSignal: (text: string) => text.includes("usage and value"), + hasContractListByCounterpartySignal: (text: string) => text.includes("contracts by counterparty"), + hasBankOperationSignal: (text: string) => text.includes("банк"), + hasDocumentSignal: (text: string) => text.includes("документ"), + hasLooseByAnchorMention: (text: string) => text.includes("по нему"), + hasHeuristicCounterpartyAnchor: (text: string) => text.includes("чепурнов"), + hasCounterpartyShipmentItemFlowSignal: (text: string) => text.includes("отгружал"), + hasImplicitCounterpartyAnchorAroundDocs: () => false, + hasGenericAddressLookupSignal: (text: string) => text.includes("покажи") +}; + +describe("addressCounterpartyIntentSignals", () => { + it("classifies open-items wording through the extracted counterparty owner", () => { + const result = resolveCounterpartyAddressIntent("хвосты покажи по счету 60 на август 2022", defaultDeps); + + expect(result?.intent).toBe("open_items_by_counterparty_or_contract"); + expect(result?.reasons).toContain("open_items_signal_detected"); + }); + + it("classifies counterparty document-flow wording through the extracted counterparty owner", () => { + const result = resolveCounterpartyAddressIntent("что нам отгружал чепурнов", defaultDeps); + + expect(result?.intent).toBe("list_documents_by_counterparty"); + expect(result?.reasons).toContain("counterparty_item_flow_signal_detected"); + }); + + it("keeps the main resolver behavior stable through counterparty-owner delegation", () => { + const result = resolveAddressIntent("хвосты покажи по счету 60 на август 2022"); + + expect(result.intent).toBe("open_items_by_counterparty_or_contract"); + }); + + it("does not steal inventory wording into the counterparty owner", () => { + const result = resolveCounterpartyAddressIntent("show inventory on hand as of 2020-03-15", defaultDeps); + + expect(result).toBeNull(); + }); +});