diff --git a/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md b/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md index 3009236..a54e230 100644 --- a/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md +++ b/docs/ARCH/11 - architecture_turnaround/06 - phase_acceptance_matrix.md @@ -27,7 +27,7 @@ Current reporting baseline: - Post-F Semantic Integrity Hardening: `99%`, operationally closed/regression gate. - Planner Autonomy Consolidation: `100%` for the declared phase83 slice. - Open-World Business Overview implementation breadth: `~99%` through Slice 25. -- Active next pressure: `Open-World Semantic Control Gate`, accepted module progress `~94%` after the first local control-gate cut. +- Active next pressure: `Open-World Semantic Control Gate`, accepted module progress `~96%` after the second local control-gate cut. ## Archived Execution Snapshot (2026-04-17) diff --git a/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md index 9398738..d225371 100644 --- a/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md +++ b/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md @@ -17,7 +17,7 @@ It did not reopen Post-F and it did not prove that the Open-World implementation From this point forward: - `~99%` for Open-World means implementation breadth through `Business Overview Missing Proof Ledger`; -- accepted module progress is `~94%` after the first local Semantic Control Gate cut, and remains below closure until the EHMO-derived subset is rerun; +- accepted module progress is `~96%` after the second local Semantic Control Gate cut, and remains below closure until the EHMO-derived subset is rerun; - the active work is control-gate hardening, not immediate expansion into more proof families. For the current execution spine, read `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`. @@ -55,7 +55,7 @@ For the current execution spine, read `23 - current_execution_spine_and_semantic - Completed active slice: `Business Overview Missing Proof Ledger`: business overview now records machine-readable hard proof gaps for accounting profit/margin, due-date debt aging, inventory reserve/liquidation quality, and vendor/procurement quality, distinguishing proxy-only evidence from reviewed routes that are not wired yet. - Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`. - Next active slice: `Open-World Semantic Control Gate`, covering garbage-anchor protection, business-overview continuation, intent dominance, frame hygiene, counterparty/organization arbitration, and final-summary answer shape. -- Active module progress: `~94% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)`. +- Active module progress: `~96% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)`. ## Reporting Rule @@ -63,7 +63,7 @@ Use these labels when reporting progress: - `Прогресс модуля: 99% (Post-F Semantic Integrity Hardening, operationally closed/regression gate)` when discussing the Post-F slice itself. - `Прогресс модуля: 100% (Planner Autonomy Consolidation, declared phase83 slice closed)` when discussing the planner-autonomy slice that was just completed. -- `Прогресс модуля: 94% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)` while discussing current module closure after the first local Semantic Control Gate cut. +- `Прогресс модуля: 96% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)` while discussing current module closure after the second local Semantic Control Gate cut. - `Open-World Business Overview implementation breadth: ~99%, semantic acceptance gate still open` when discussing only the already wired Slice 25 breadth. - `Прогресс модуля: X% (Open-World Bounded Autonomy Breadth, active slice: )` for later breadth work after the Semantic Control Gate is accepted. diff --git a/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md index 819105a..7bba8bb 100644 --- a/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md +++ b/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md @@ -779,7 +779,7 @@ Suggested first subset: Current status: - implementation breadth through Slice 25: `~99%`; -- accepted Open-World module progress after the first local Semantic Control Gate cut: `~94%`; +- accepted Open-World module progress after the second local Semantic Control Gate cut: `~96%`; - exact P&L, real due-date debt aging, reserve/write-off/liquidation evidence, and vendor-risk engines stay queued behind this semantic gate. ### Slice 26 local cut 1 - anchor hygiene and overview continuation @@ -798,8 +798,24 @@ Validation: - `npm.cmd test -- addressCounterpartyIntentSignals.test.ts addressQueryRuntimeM23.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts addressIntentResolverRegression.test.ts`: passed `515` with `6` skipped. - `npm.cmd run build`: passed. +### Slice 26 local cut 2 - intent dominance, frame reset, and final-summary control + +Implemented now: + +- paired receivables/payables wording such as "debtors and creditors as of today" is deferred into `business_overview` instead of collapsing into only payables or only receivables; +- raw metadata scope on an explicit topic switch, such as "now what registers and fields exist for VAT?", overrides stale document/value-flow subjects and suppresses stale follow-up date scope; +- final next-step summary wording after a business overview stays in the bounded overview lane instead of becoming a fresh wrong documents/counterparty lookup; +- business-overview continuation now suppresses stale follow-up counterparties before they can leak into the new summary frame. + +Local validation is accepted for this cut: + +- `npm.cmd test -- assistantMcpDiscoveryTurnInputAdapter.test.ts addressIntentResolverRegression.test.ts`: passed `98` with `6` skipped. +- `npm.cmd test -- addressCounterpartyIntentSignals.test.ts addressQueryRuntimeM23.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts addressIntentResolverRegression.test.ts`: passed `519` with `6` skipped. +- `npm.cmd run build`: passed. +- graphify rebuild after Slice 26 local cut 2: `6076 nodes`, `13247 edges`, `138 communities`. + Remaining before acceptance: - rerun the EHMO-derived semantic subset; -- continue W3/W4/W5/W6 for intent dominance, frame reset, counterparty/organization arbitration, and final-summary answer lane; +- continue W5 and any EHMO-revealed W3/W4/W6 gaps for counterparty/organization arbitration, remaining wrong-lane prevention, frame reset, and final-summary answer lane; - only then rerun the fat manual GUI pack for acceptance. diff --git a/docs/ARCH/11 - architecture_turnaround/23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md b/docs/ARCH/11 - architecture_turnaround/23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md index 7425859..18c1c5b 100644 --- a/docs/ARCH/11 - architecture_turnaround/23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md +++ b/docs/ARCH/11 - architecture_turnaround/23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md @@ -66,12 +66,12 @@ This gate is not a request to tune the assistant for every weird question in tha Current status should be reported as: - implementation breadth: `~99%` for Open-World Business Overview through Slice 25; -- accepted module progress: `~94% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)`. +- accepted module progress: `~96% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)`. -This is not a regression from `99%` to `94%`. It is a metric split: +This is not a regression from `99%` to `96%`. It is a metric split: - `99%` describes wired breadth; -- `94%` describes closure confidence after the first local control-gate cut; the gate is still not accepted without EHMO-subset replay. +- `96%` describes closure confidence after the second local control-gate cut; the gate is still not accepted without EHMO-subset replay. ## Current Local Cut @@ -81,6 +81,13 @@ Local cut 1 is implemented: - W2 starter: business-overview continuation wording keeps the bounded overview lane after a previous overview run. - W3 edge covered locally: VAT-position wording inside a business-overview continuation does not fall into inventory selected-object routing. +Local cut 2 is implemented: + +- W3 extension: paired receivables/payables wording is routed to bounded business overview debt-position analysis instead of one-sided exact debt. +- W4 starter: explicit metadata topic switches let raw metadata scope override stale document/value-flow subjects and suppress stale date scope. +- W6 starter: final next-step summary wording after business overview stays in the bounded overview lane and does not invent document/counterparty subjects. +- Business-overview frame hygiene now suppresses stale follow-up counterparties when the current turn is a broad overview continuation. + This is local evidence only. It improves the gate but does not close it until the EHMO-derived subset and the fat manual pack are semantically reviewed again. ## Failure Classes To Fix @@ -120,7 +127,7 @@ Each work unit should add focused local tests and then be validated against the ## Acceptance Gate -The current module can move from `~94%` toward closure only after: +The current module can move from `~96%` toward closure only after: - the EHMO-derived critical subset is rerun and semantically reviewed; - old canaries remain green: Post-F, phase83, inventory selected-object, VAT continuity, SVK document/movement chains; @@ -150,15 +157,15 @@ Manual runtime run reviewed as the gate opener: Graphify snapshot at this status cut: -- `6070 nodes` -- `13233 edges` -- `135 communities` +- `6076 nodes` +- `13247 edges` +- `138 communities` ## Reporting Rule Until the semantic control gate is accepted, use: -`Прогресс модуля: 94% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)` +`Прогресс модуля: 96% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)` If discussing only the already wired business-overview breadth, say: diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index f36ee5f..f5f9bef 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -77,7 +77,7 @@ Status canon for planning: - The current completed breadth slice is `Business Overview Missing Proof Ledger`: business overview now records hard remaining proof gaps as machine-readable `missing_proof_families`, separating proxy-only evidence from reviewed routes that are not wired yet. - The `assistant-stage1-EHMOy3lNFt` manual GUI replay opened the next acceptance gate: `Open-World Semantic Control Gate`. - The `~99%` Open-World number now means implementation breadth through Slice 25, not accepted semantic closure under broad human dialogue pressure. -- The next active breadth slice is semantic control rather than new proof-family expansion: garbage-anchor protection, business-overview continuation, intent dominance, frame hygiene, counterparty/organization arbitration, and final-summary answer shape. +- The active breadth slice is semantic control rather than new proof-family expansion: garbage-anchor protection, business-overview continuation, intent dominance, frame hygiene, counterparty/organization arbitration, and final-summary answer shape. - The short source of truth for status wording is [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md). - The current execution spine after EHMO is [23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md](./23%20-%20current_execution_spine_and_semantic_control_gate_2026-05-05.md). @@ -144,11 +144,11 @@ Current honest status: - bounded-autonomy foundation readiness: `~89%` - open-world bounded-autonomy readiness: `~87%` - active Open-World Bounded Autonomy Breadth implementation breadth: `~99%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, debt staleness-risk proxy bridged locally, debt due-date boundary arbitration bridged locally, inventory reserve/liquidation boundary arbitration bridged locally, supplier/procurement-quality boundary arbitration bridged locally, supplier concentration proxy bridged locally, document/account-section activity profile bridged locally, counterparty population/roles and contract usage profiles bridged locally, yearly operating-flow proxy bridged locally, earnings/best-year wording arbitration bridged locally, profit/margin wording boundary arbitration bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, gap-specific answer shaping bridged locally, and missing proof families recorded as runtime evidence ledger; exact accounting profit/margin, true due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence are still pending -- active Open-World Bounded Autonomy Breadth accepted-module progress: `~94%`, because the first local `Open-World Semantic Control Gate` cut is implemented, but EHMO-subset replay is still pending +- active Open-World Bounded Autonomy Breadth accepted-module progress: `~96%`, because local `Open-World Semantic Control Gate` cuts now cover anchor hygiene, overview continuation, debt-position intent dominance, metadata topic-switch frame reset, and final-summary continuation, but EHMO-subset replay is still pending - Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice - active inventory-stock breadth slice progress: `100%` for the declared scenario pack, not for arbitrary inventory questions - Planner Autonomy Consolidation progress: `100%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, unambiguous metadata-surface lane inference, catalog chain-template scoring, structured chain-match contract exposure, runtime/debug propagation, subject-aware bidirectional comparison arbitration, structured catalog-alignment verdicts, representative alignment regression guard, catalog-alignment reason-code telemetry, explicit `alignment_status` propagation, truth-harness/acceptance-matrix surfacing, soft divergence warning, `catalog_alignment_ok` acceptance invariant, step-level expected catalog-alignment assertions, phase66 and phase32 spec alignment expectations, AGENT source-catalog surfacing, generated phase83 mixed planner-brain replay spec, checked-source user-facing error sanitation, surface-grounded catalog promotion, and guarded live phase83 acceptance validated. Broader unfamiliar 1C asks are now next-module breadth work rather than an open blocker inside this declared slice -- graph snapshot after latest rebuild: `6070 nodes`, `13233 edges`, `135 communities` +- graph snapshot after latest rebuild: `6076 nodes`, `13247 edges`, `138 communities` - current regression-gate breakpoint: - the validated hot paths are no longer structurally broken; - flagship continuity collapse is no longer the primary risk; @@ -203,6 +203,7 @@ Latest live proof now includes: - business-overview supplier concentration proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6041 nodes`, `13162 edges`, `136 communities`; the proxy ranks confirmed outgoing payment counterparties while vendor risk, procurement quality, and full expense structure remain unclaimed - business-overview yearly operating-flow proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6047 nodes`, `13177 edges`, `139 communities`; the proxy builds annual incoming/outgoing/net buckets from confirmed money-flow rows while profit, финрезультат, and full P&L remain unclaimed - business-overview missing proof ledger accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `416/416`; build passed; graphify count is recorded in the current graph snapshot; hard remaining proof gaps are now visible as machine-readable `missing_proof_families` rather than only prose warnings +- semantic control gate local cuts accepted locally: adapter/resolver focused regressions passed `98/98` with `6` skipped; broader address counterparty/M23/adapter/resolver slice passed `519/519` with `6` skipped; build passed; graphify rebuilt to `6076 nodes`, `13247 edges`, `138 communities`; EHMO-derived semantic replay remains the acceptance gate - business-overview earnings wording arbitration accepted locally: turn-meaning/turn-input slice passed `85/85` with `6` skipped; M23 route/runtime regression passed `412/412`; runtime-entry/pilot/answer slice passed `85/85` with `3` skipped; build passed; graphify rebuilt to `6052 nodes`, `13187 edges`, `138 communities`; organization-level earnings/best-year wording now reaches `business_overview` while explicit customer/counterparty ranking remains in exact customer value routes - inventory template lift accepted locally: catalog/data-need/planner/turn-input slice passed `139/139` with `6` skipped; full MCP-discovery slice passed `276/276` with `9` skipped; build passed; graphify stayed at `5912 nodes`, `12833 edges`, `138 communities` - inventory runtime-boundary hardening accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `68/68` with `1` skipped; full MCP-discovery slice passed `277/277` with `9` skipped; build passed; graphify rebuilt to `5913 nodes`, `12837 edges`, `138 communities` diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index faf1db1..6030d76 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1529,6 +1529,16 @@ function hasOrganizationLevelDebtDueDateOverviewBridgeSignal(text) { const hasCompanyScopeCue = /(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u043e\u0431\u0449\w*|\u043a\u0430\u043a\w*|\u043f\u043e\u043a\u0430\u0436|\u0434\u0430\u0439|\u0441\u0440\u0435\u0437|\u0430\u043d\u0430\u043b\u0438\u0437|(?:19|20)\d{2}|company|business|organization|overall|our|we|us|show|give|analysis)/iu.test(normalized); return hasDueDateDebtCue && hasCompanyScopeCue; } +function hasOrganizationLevelDebtPositionOverviewBridgeSignal(text) { + const normalized = String(text ?? "").trim().toLowerCase(); + if (!normalized) { + return false; + } + const hasReceivablesCue = /(?:\u0434\u0435\u0431\u0438\u0442\u043e\u0440\w*|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\w*|receivables?|accounts\s+receivable)/iu.test(normalized); + const hasPayablesCue = /(?:\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\w*|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436|\u043c\u044b\s+\u0434\u043e\u043b\u0436\w*|payables?|accounts\s+payable)/iu.test(normalized); + const hasOverviewCue = /(?:\u0441\u0440\u0435\u0437|\u043f\u043e\u0437\u0438\u0446\w*|\u0431\u0430\u043b\u0430\u043d\u0441|\u0441\u0430\u043b\u044c\u0434\u043e|\u043d\u0435\u0442\u0442\u043e|\u043d\u0430\s+\u0441\u0435\u0433\u043e\u0434\u043d|\u043d\u0430\s+\u0434\u0430\u0442\u0443|\u0443\s+\u043d\u0430\u0441|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0431\u0437\u043e\u0440|\u0430\u043d\u0430\u043b\u0438\u0437|as\s+of|overview|balance|net|company|business)/iu.test(normalized); + return hasReceivablesCue && hasPayablesCue && hasOverviewCue; +} function hasOrganizationLevelInventoryReserveLiquidationOverviewBridgeSignal(text) { const normalized = String(text ?? "").trim().toLowerCase(); if (!normalized) { @@ -1727,6 +1737,9 @@ function resolveUnicodeAddressIntentBridge(text) { if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) { return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery"); } + if (hasOrganizationLevelDebtPositionOverviewBridgeSignal(normalized)) { + return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_debt_position_deferred_to_discovery"); + } if (hasOrganizationLevelDebtDueDateOverviewBridgeSignal(normalized)) { return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_debt_due_date_deferred_to_discovery"); } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index bbb9873..890d2e2 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -579,6 +579,15 @@ function hasOrganizationLevelDebtDueDateOverviewSignal(text) { const hasCompanyScopeCue = /(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u043e\u0431\u0449\w*|\u043a\u0430\u043a\w*|\u043f\u043e\u043a\u0430\u0436|\u0434\u0430\u0439|\u0441\u0440\u0435\u0437|\u0430\u043d\u0430\u043b\u0438\u0437|(?:19|20)\d{2}|company|business|organization|overall|our|we|us|show|give|analysis)/iu.test(text); return hasDueDateDebtCue && hasCompanyScopeCue; } +function hasOrganizationLevelDebtPositionOverviewSignal(text) { + if (!text) { + return false; + } + const hasReceivablesCue = /(?:\u0434\u0435\u0431\u0438\u0442\u043e\u0440\w*|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\w*|receivables?|accounts\s+receivable)/iu.test(text); + const hasPayablesCue = /(?:\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\w*|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436|\u043c\u044b\s+\u0434\u043e\u043b\u0436\w*|payables?|accounts\s+payable)/iu.test(text); + const hasOverviewCue = /(?:\u0441\u0440\u0435\u0437|\u043f\u043e\u0437\u0438\u0446\w*|\u0431\u0430\u043b\u0430\u043d\u0441|\u0441\u0430\u043b\u044c\u0434\u043e|\u043d\u0435\u0442\u0442\u043e|\u043d\u0430\s+\u0441\u0435\u0433\u043e\u0434\u043d|\u043d\u0430\s+\u0434\u0430\u0442\u0443|\u0443\s+\u043d\u0430\u0441|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0431\u0437\u043e\u0440|\u0430\u043d\u0430\u043b\u0438\u0437|as\s+of|overview|balance|net|company|business)/iu.test(text); + return hasReceivablesCue && hasPayablesCue && hasOverviewCue; +} function hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) { if (!text) { return false; @@ -605,6 +614,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text) { } function hasBusinessOverviewSignal(text) { if (hasOrganizationLevelEarningsOverviewSignal(text) || + hasOrganizationLevelDebtPositionOverviewSignal(text) || hasOrganizationLevelDebtDueDateOverviewSignal(text) || hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) || hasOrganizationLevelSupplierQualityOverviewSignal(text)) { @@ -620,7 +630,11 @@ function hasBusinessOverviewContinuationSignal(text) { const hasEvidenceContinuationCue = /(?:по\s+этим\s+данн|на\s+базе\s+этих\s+данн|из\s+этого|по\s+итогам|что\s+подтвержден|чего\s+не\s+хватает|что\s+пока\s+нельзя|что\s+можно\s+сказать|какой\s+вывод|итогов|резюм|вывод)/iu.test(normalized); const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|налогов\p{L}*\s+вывод)/iu.test(normalized); const hasTaxContinuationCue = /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test(normalized); - return hasEvidenceContinuationCue || hasAnalystContinuationCue || hasTaxContinuationCue; + const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized); + return hasEvidenceContinuationCue || hasAnalystContinuationCue || hasTaxContinuationCue || hasFinalSummaryCue; +} +function hasExplicitTopicSwitchSignal(text) { + return /(?:^|\s)(?:\u0442\u0435\u043f\u0435\u0440\u044c|\u0430\s+\u0442\u0435\u043f\u0435\u0440\u044c|\u0434\u0430\u043b\u044c\u0448\u0435|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e|\u043f\u0435\u0440\u0435\u0439\u0434[\u0451\u0435]\u043c|\u0441\u043c\u0435\u043d\u0438\u043c\s+\u0442\u0435\u043c\u0443|\u0432\u0435\u0440\u043d[\u0451\u0435]\u043c\u0441\u044f\s+\u043a|now|next|switch\s+to)(?:\s|$)/iu.test(text); } function hasBusinessOverviewFollowupSeed(followupSeed) { return (followupSeed.pilotScope === "business_overview_route_template_v1" || @@ -997,6 +1011,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText); const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText); const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null; + const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText); const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null; const entityResolutionClarificationCandidate = followupSeed.pilotScope === "entity_resolution_search_v1" && followupSeed.entityResolutionStatus === "ambiguous" @@ -1044,9 +1059,22 @@ function buildAssistantMcpDiscoveryTurnInput(input) { (isInvalidEntityCandidate(followupSeed.counterparty) || sameScopedName(followupSeed.counterparty, followupSeed.organization) || sameScopedName(followupSeed.counterparty, currentTurnOrganizationScope))); + const businessOverviewSuppressesFollowupCounterparty = Boolean(businessOverviewSignal && + (rawBusinessOverviewSignal || + businessOverviewContinuationSignal || + broadBusinessEvaluationUnsupported || + rawDomain === "business_summary" || + rawDomain === "business_overview" || + rawAction === "broad_evaluation")); const effectiveFollowupCounterparty = followupCounterpartyIsMetadataOrganizationScope ? null - : followupSeed.counterparty; + : businessOverviewSuppressesFollowupCounterparty + ? null + : followupSeed.counterparty; + const rawMetadataScopeOverridesFollowupEntity = Boolean(rawMetadataSignal && + rawMetadataScopeHint && + (effectiveFollowupCounterparty || followupSeed.discoveryEntity) && + !sameScopedName(rawMetadataScopeHint, effectiveFollowupCounterparty ?? followupSeed.discoveryEntity)); const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope); const organizationClarificationFollowupApplicable = Boolean(followupSeed.domain === "counterparty_value" && !effectiveFollowupCounterparty && @@ -1095,6 +1123,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { hasMetadataObjectHint(rawText)); const metadataLaneCarryoverAvailable = Boolean(effectiveFollowupCounterparty || followupSeed.discoveryEntity || + rawMetadataScopeHint || followupSeed.metadataScopeHint || followupSeed.metadataSelectedEntitySet || followupSeed.metadataSelectedSurfaceObjects.length > 0); @@ -1376,9 +1405,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) { ? null : rawEntitySearchOverridesStaleScope ? null - : explicitCurrentCounterpartyOverridesFollowupEntity + : rawMetadataScopeOverridesFollowupEntity ? null - : effectiveFollowupCounterparty ?? followupSeed.discoveryEntity; + : explicitCurrentCounterpartyOverridesFollowupEntity + ? null + : effectiveFollowupCounterparty ?? followupSeed.discoveryEntity; const entityCandidates = entityResolutionSignal ? [] : []; if (entityResolutionSignal) { pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate); @@ -1404,8 +1435,10 @@ function buildAssistantMcpDiscoveryTurnInput(input) { } pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity); if (!groundedFollowupEntity) { - pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null); - if (!metadataScopedLaneWithoutSubject) { + if (!rawMetadataScopeOverridesFollowupEntity) { + pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null); + } + if (!metadataScopedLaneWithoutSubject && !rawMetadataScopeOverridesFollowupEntity) { pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null); } } @@ -1414,7 +1447,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !groundedFollowupEntity && !metadataScopedLaneWithoutSubject) { - pushUnique(entityCandidates, followupSeed.discoveryEntity); + if (!rawMetadataScopeOverridesFollowupEntity) { + pushUnique(entityCandidates, followupSeed.discoveryEntity); + } pushUnique(entityCandidates, rawMetadataScopeHint); } const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !effectiveFollowupCounterparty; @@ -1479,6 +1514,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { openScopeValueFlowWithoutResolvedCounterparty || (valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal)))); const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal); + const topicSwitchSuppressesFollowupScope = Boolean(rawTopicSwitchSignal && + (rawMetadataSignal || + businessOverviewSignal || + rawEntitySearchOverridesStaleScope || + explicitOrganizationScopeSignal || + rawAllTimeScopeSignal)); const normalizedPredecomposeDateScope = (rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) || suppressNegatedTaxOnlyDateScope || businessOverviewRawWithoutDateScope || @@ -1492,6 +1533,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : assistantTurnMeaningDateScope; const normalizedFollowupDateScope = rawEntitySearchOverridesStaleScope || suppressNegatedTaxOnlyDateScope || + topicSwitchSuppressesFollowupScope || (suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope)) ? null : followupSeed.dateScope; @@ -1698,6 +1740,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (rawMetadataScopeHint) { pushReason(reasonCodes, "mcp_discovery_metadata_scope_hint_from_raw_text"); } + if (rawMetadataScopeOverridesFollowupEntity) { + pushReason(reasonCodes, "mcp_discovery_raw_metadata_scope_overrides_stale_entity"); + } + if (topicSwitchSuppressesFollowupScope) { + pushReason(reasonCodes, "mcp_discovery_topic_switch_suppressed_followup_scope"); + } if (rawEntityCandidate) { pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search"); } @@ -1809,11 +1857,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (businessOverviewContinuationSignal) { pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context"); } + if (businessOverviewSuppressesFollowupCounterparty) { + pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty"); + } if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) && normalizedPredecomposeCounterparty) { pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose"); } - if (effectiveFollowupCounterparty && !rawEntitySearchOverridesStaleScope) { + if (effectiveFollowupCounterparty && + !rawEntitySearchOverridesStaleScope && + !rawMetadataScopeOverridesFollowupEntity) { pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context"); } if (followupDateScopeApplied) { diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index cc0d353..029a111 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -1920,6 +1920,26 @@ function hasOrganizationLevelDebtDueDateOverviewBridgeSignal(text: string): bool return hasDueDateDebtCue && hasCompanyScopeCue; } +function hasOrganizationLevelDebtPositionOverviewBridgeSignal(text: string): boolean { + const normalized = String(text ?? "").trim().toLowerCase(); + if (!normalized) { + return false; + } + const hasReceivablesCue = + /(?:\u0434\u0435\u0431\u0438\u0442\u043e\u0440\w*|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\w*|receivables?|accounts\s+receivable)/iu.test( + normalized + ); + const hasPayablesCue = + /(?:\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\w*|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436|\u043c\u044b\s+\u0434\u043e\u043b\u0436\w*|payables?|accounts\s+payable)/iu.test( + normalized + ); + const hasOverviewCue = + /(?:\u0441\u0440\u0435\u0437|\u043f\u043e\u0437\u0438\u0446\w*|\u0431\u0430\u043b\u0430\u043d\u0441|\u0441\u0430\u043b\u044c\u0434\u043e|\u043d\u0435\u0442\u0442\u043e|\u043d\u0430\s+\u0441\u0435\u0433\u043e\u0434\u043d|\u043d\u0430\s+\u0434\u0430\u0442\u0443|\u0443\s+\u043d\u0430\u0441|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0431\u0437\u043e\u0440|\u0430\u043d\u0430\u043b\u0438\u0437|as\s+of|overview|balance|net|company|business)/iu.test( + normalized + ); + return hasReceivablesCue && hasPayablesCue && hasOverviewCue; +} + function hasOrganizationLevelInventoryReserveLiquidationOverviewBridgeSignal(text: string): boolean { const normalized = String(text ?? "").trim().toLowerCase(); if (!normalized) { @@ -2247,6 +2267,10 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery"); } + if (hasOrganizationLevelDebtPositionOverviewBridgeSignal(normalized)) { + return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_debt_position_deferred_to_discovery"); + } + if (hasOrganizationLevelDebtDueDateOverviewBridgeSignal(normalized)) { return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_debt_due_date_deferred_to_discovery"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index a96bc9d..c717251 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -783,6 +783,25 @@ function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean { return hasDueDateDebtCue && hasCompanyScopeCue; } +function hasOrganizationLevelDebtPositionOverviewSignal(text: string): boolean { + if (!text) { + return false; + } + const hasReceivablesCue = + /(?:\u0434\u0435\u0431\u0438\u0442\u043e\u0440\w*|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\w*|receivables?|accounts\s+receivable)/iu.test( + text + ); + const hasPayablesCue = + /(?:\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\w*|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436|\u043c\u044b\s+\u0434\u043e\u043b\u0436\w*|payables?|accounts\s+payable)/iu.test( + text + ); + const hasOverviewCue = + /(?:\u0441\u0440\u0435\u0437|\u043f\u043e\u0437\u0438\u0446\w*|\u0431\u0430\u043b\u0430\u043d\u0441|\u0441\u0430\u043b\u044c\u0434\u043e|\u043d\u0435\u0442\u0442\u043e|\u043d\u0430\s+\u0441\u0435\u0433\u043e\u0434\u043d|\u043d\u0430\s+\u0434\u0430\u0442\u0443|\u0443\s+\u043d\u0430\u0441|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0431\u0437\u043e\u0440|\u0430\u043d\u0430\u043b\u0438\u0437|as\s+of|overview|balance|net|company|business)/iu.test( + text + ); + return hasReceivablesCue && hasPayablesCue && hasOverviewCue; +} + function hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text: string): boolean { if (!text) { return false; @@ -838,6 +857,7 @@ function hasOrganizationLevelSupplierQualityOverviewSignal(text: string): boolea function hasBusinessOverviewSignal(text: string): boolean { if ( hasOrganizationLevelEarningsOverviewSignal(text) || + hasOrganizationLevelDebtPositionOverviewSignal(text) || hasOrganizationLevelDebtDueDateOverviewSignal(text) || hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) || hasOrganizationLevelSupplierQualityOverviewSignal(text) @@ -866,7 +886,17 @@ function hasBusinessOverviewContinuationSignal(text: string): boolean { /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test( normalized ); - return hasEvidenceContinuationCue || hasAnalystContinuationCue || hasTaxContinuationCue; + const hasFinalSummaryCue = + /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test( + normalized + ); + return hasEvidenceContinuationCue || hasAnalystContinuationCue || hasTaxContinuationCue || hasFinalSummaryCue; +} + +function hasExplicitTopicSwitchSignal(text: string): boolean { + return /(?:^|\s)(?:\u0442\u0435\u043f\u0435\u0440\u044c|\u0430\s+\u0442\u0435\u043f\u0435\u0440\u044c|\u0434\u0430\u043b\u044c\u0448\u0435|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e|\u043f\u0435\u0440\u0435\u0439\u0434[\u0451\u0435]\u043c|\u0441\u043c\u0435\u043d\u0438\u043c\s+\u0442\u0435\u043c\u0443|\u0432\u0435\u0440\u043d[\u0451\u0435]\u043c\u0441\u044f\s+\u043a|now|next|switch\s+to)(?:\s|$)/iu.test( + text + ); } function hasBusinessOverviewFollowupSeed(followupSeed: ReturnType): boolean { @@ -1387,6 +1417,7 @@ export function buildAssistantMcpDiscoveryTurnInput( const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText); const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText); const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null; + const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText); const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null; const entityResolutionClarificationCandidate = followupSeed.pilotScope === "entity_resolution_search_v1" && @@ -1447,9 +1478,26 @@ export function buildAssistantMcpDiscoveryTurnInput( sameScopedName(followupSeed.counterparty, followupSeed.organization) || sameScopedName(followupSeed.counterparty, currentTurnOrganizationScope)) ); + const businessOverviewSuppressesFollowupCounterparty = Boolean( + businessOverviewSignal && + (rawBusinessOverviewSignal || + businessOverviewContinuationSignal || + broadBusinessEvaluationUnsupported || + rawDomain === "business_summary" || + rawDomain === "business_overview" || + rawAction === "broad_evaluation") + ); const effectiveFollowupCounterparty = followupCounterpartyIsMetadataOrganizationScope + ? null + : businessOverviewSuppressesFollowupCounterparty ? null : followupSeed.counterparty; + const rawMetadataScopeOverridesFollowupEntity = Boolean( + rawMetadataSignal && + rawMetadataScopeHint && + (effectiveFollowupCounterparty || followupSeed.discoveryEntity) && + !sameScopedName(rawMetadataScopeHint, effectiveFollowupCounterparty ?? followupSeed.discoveryEntity) + ); const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope); const organizationClarificationFollowupApplicable = Boolean( followupSeed.domain === "counterparty_value" && @@ -1514,6 +1562,7 @@ export function buildAssistantMcpDiscoveryTurnInput( const metadataLaneCarryoverAvailable = Boolean( effectiveFollowupCounterparty || followupSeed.discoveryEntity || + rawMetadataScopeHint || followupSeed.metadataScopeHint || followupSeed.metadataSelectedEntitySet || followupSeed.metadataSelectedSurfaceObjects.length > 0 @@ -1852,6 +1901,8 @@ export function buildAssistantMcpDiscoveryTurnInput( ? null : rawEntitySearchOverridesStaleScope ? null + : rawMetadataScopeOverridesFollowupEntity + ? null : explicitCurrentCounterpartyOverridesFollowupEntity ? null : effectiveFollowupCounterparty ?? followupSeed.discoveryEntity; @@ -1879,8 +1930,10 @@ export function buildAssistantMcpDiscoveryTurnInput( } pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity); if (!groundedFollowupEntity) { - pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null); - if (!metadataScopedLaneWithoutSubject) { + if (!rawMetadataScopeOverridesFollowupEntity) { + pushScopedEntityCandidate(entityCandidates, effectiveFollowupCounterparty, null); + } + if (!metadataScopedLaneWithoutSubject && !rawMetadataScopeOverridesFollowupEntity) { pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null); } } @@ -1891,7 +1944,9 @@ export function buildAssistantMcpDiscoveryTurnInput( !groundedFollowupEntity && !metadataScopedLaneWithoutSubject ) { - pushUnique(entityCandidates, followupSeed.discoveryEntity); + if (!rawMetadataScopeOverridesFollowupEntity) { + pushUnique(entityCandidates, followupSeed.discoveryEntity); + } pushUnique(entityCandidates, rawMetadataScopeHint); } const openScopeValueFlowWithoutCounterparty = @@ -1977,6 +2032,14 @@ export function buildAssistantMcpDiscoveryTurnInput( (valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))) ); const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal); + const topicSwitchSuppressesFollowupScope = Boolean( + rawTopicSwitchSignal && + (rawMetadataSignal || + businessOverviewSignal || + rawEntitySearchOverridesStaleScope || + explicitOrganizationScopeSignal || + rawAllTimeScopeSignal) + ); const normalizedPredecomposeDateScope = (rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) || suppressNegatedTaxOnlyDateScope || @@ -1993,6 +2056,7 @@ export function buildAssistantMcpDiscoveryTurnInput( const normalizedFollowupDateScope = rawEntitySearchOverridesStaleScope || suppressNegatedTaxOnlyDateScope || + topicSwitchSuppressesFollowupScope || (suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope)) ? null : followupSeed.dateScope; @@ -2219,6 +2283,12 @@ export function buildAssistantMcpDiscoveryTurnInput( if (rawMetadataScopeHint) { pushReason(reasonCodes, "mcp_discovery_metadata_scope_hint_from_raw_text"); } + if (rawMetadataScopeOverridesFollowupEntity) { + pushReason(reasonCodes, "mcp_discovery_raw_metadata_scope_overrides_stale_entity"); + } + if (topicSwitchSuppressesFollowupScope) { + pushReason(reasonCodes, "mcp_discovery_topic_switch_suppressed_followup_scope"); + } if (rawEntityCandidate) { pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search"); } @@ -2330,13 +2400,20 @@ export function buildAssistantMcpDiscoveryTurnInput( if (businessOverviewContinuationSignal) { pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context"); } + if (businessOverviewSuppressesFollowupCounterparty) { + pushReason(reasonCodes, "mcp_discovery_business_overview_suppressed_stale_counterparty"); + } if ( !(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) && normalizedPredecomposeCounterparty ) { pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose"); } - if (effectiveFollowupCounterparty && !rawEntitySearchOverridesStaleScope) { + if ( + effectiveFollowupCounterparty && + !rawEntitySearchOverridesStaleScope && + !rawMetadataScopeOverridesFollowupEntity + ) { pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context"); } if (followupDateScopeApplied) { diff --git a/llm_normalizer/backend/tests/addressIntentResolverRegression.test.ts b/llm_normalizer/backend/tests/addressIntentResolverRegression.test.ts index ba658f8..6f18d21 100644 --- a/llm_normalizer/backend/tests/addressIntentResolverRegression.test.ts +++ b/llm_normalizer/backend/tests/addressIntentResolverRegression.test.ts @@ -36,6 +36,15 @@ describe("addressIntentResolver regression bridges", () => { expect(result.reasons).toContain("unicode_business_overview_earnings_deferred_to_discovery"); }); + it("defers paired receivables and payables wording to business overview instead of one debt side", () => { + const result = resolveAddressIntent( + "\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0440\u0435\u0437 \u0434\u0435\u0431\u0438\u0442\u043e\u0440\u043a\u0438 \u0438 \u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\u043a\u0438 \u043d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438" + ); + + expect(result.intent).toBe("unknown"); + expect(result.reasons).toContain("unicode_business_overview_debt_position_deferred_to_discovery"); + }); + it("detects specific counterparty turnover wording as revenue profile", () => { const result = resolveAddressIntent( "\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a" diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 2dba6fc..4b85b79 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -2648,6 +2648,115 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context"); }); + it("routes paired receivables and payables wording to business overview instead of one debt side", () => { + const orgName = + "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: + "\u041f\u043e\u043a\u0430\u0436\u0438 \u0441\u0440\u0435\u0437 \u0434\u0435\u0431\u0438\u0442\u043e\u0440\u043a\u0438 \u0438 \u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\u043a\u0438 \u043d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438", + assistantTurnMeaning: { + asked_domain_family: "counterparty", + asked_action_family: "debt_snapshot", + explicit_intent_candidate: "payables_confirmed_as_of_date" + }, + predecomposeContract: { + entities: { organization: orgName }, + period: { as_of_date: "2026-05-05" } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation"); + expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "business_overview", + asked_action_family: "broad_evaluation", + explicit_organization_scope: orgName, + unsupported_but_understood_family: "broad_business_evaluation", + stale_replay_forbidden: true + }); + }); + + it("lets raw metadata scope override stale document evidence subject on topic switch", () => { + const orgName = + "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; + const staleCounterparty = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"; + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: + "\u0422\u0435\u043f\u0435\u0440\u044c \u043a\u0430\u043a\u0438\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b \u0438 \u043f\u043e\u043b\u044f \u0435\u0441\u0442\u044c \u043f\u043e \u041d\u0414\u0421?", + assistantTurnMeaning: { + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_intent_candidate: "list_documents_by_counterparty" + }, + followupContext: { + previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1", + previous_discovery_entity_candidates: [staleCounterparty], + previous_filters: { + counterparty: staleCounterparty, + organization: orgName, + period_from: "2020-01-01", + period_to: "2020-12-31" + } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("1C metadata evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "metadata", + metadata_scope_hint: "\u041d\u0414\u0421", + stale_replay_forbidden: true + }); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["\u041d\u0414\u0421"]); + expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined(); + expect(result.data_need_graph?.subject_candidates).toEqual(["\u041d\u0414\u0421"]); + expect(result.reason_codes).toContain("mcp_discovery_raw_metadata_scope_overrides_stale_entity"); + expect(result.reason_codes).toContain("mcp_discovery_topic_switch_suppressed_followup_scope"); + expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context"); + }); + + it("keeps final next-step summary wording in business overview after stale document meaning", () => { + const orgName = + "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: + "\u0427\u0442\u043e \u043c\u044b \u0437\u043d\u0430\u0435\u043c \u043f\u043e \u0438\u0442\u043e\u0433\u0443 \u0438 \u0447\u0442\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435?", + assistantTurnMeaning: { + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_intent_candidate: "list_documents_by_counterparty", + explicit_entity_candidates: ["\u0438\u0442\u043e\u0433\u0443"] + }, + followupContext: { + previous_discovery_pilot_scope: "business_overview_route_template_v1", + previous_filters: { + organization: orgName, + period_from: "2020-01-01", + period_to: "2020-12-31" + } + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation"); + expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); + expect(result.data_need_graph?.subject_candidates).toEqual([]); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "business_overview", + asked_action_family: "broad_evaluation", + explicit_organization_scope: orgName, + explicit_date_scope: "2020", + unsupported_but_understood_family: "broad_business_evaluation", + stale_replay_forbidden: true + }); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context"); + }); + it("continues business overview on by-these-data profit wording without grounding pseudo anchors", () => { const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";