diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index e40f04b..f492383 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -140,6 +140,7 @@ Completed in the current working pass: - exact address intents can now stay in the address lane even if the semantic guard overflags deep investigation without an actual investigative user request; - selected-object inventory follow-ups can now override a stale stock root intent when the semantic contract already marks `selected_object_scope_detected`, including exact user wording like `по выбранному объекту ... где взяли это`; - explicit capability-meta wording for `дельта по договорам` now keeps the asked capability in the user-facing answer instead of collapsing into the generic `что ты умеешь` catalog reply. +- the transition hot path now starts consuming the shared continuity snapshot as fallback authority for active item / active organization / grounded inventory root frame instead of rebuilding those values only from local ad hoc history scans; - live replay `address_truth_harness_phase7_meta_domain_mix_live_20260417_post_arch_fix_rerun2` is accepted end-to-end with `14/14` steps green, including the previously broken `step_01_counterparty_documents` and `step_04_open_items_account_60`. Still open after this pass: @@ -233,6 +234,30 @@ Still open after the accepted phase11 replay: - answer shaping on some long exact list answers is still heavier than the target human product feel, even though the truth path and routing are now correct; - the next architecture slice should move to wider saved-session acceptance coverage and humanized exact-answer presentation, not back to isolated prompt-level repairs. +## Next Execution Slice (2026-04-18) + +The project is now moving from: + +- `breakpoint recovery` + +to: + +- `danger-zone exit under explicit gates` + +This next slice should be executed in the following order: + +1. Finish continuity authority convergence in the hot runtime path. +2. Widen saved-session replay coverage beyond the already repaired flagship chains. +3. Tighten human answer shaping on long exact answers without reintroducing template drift. +4. Only after that, begin controlled domain-by-domain expansion toward the multi-domain stage. + +Current explicit goals for this slice: + +- fewer owners independently reconstruct `active context`; +- more replay breadth before any large expansion claim; +- cleaner user-facing business answers on already-correct truth paths; +- lower risk that new domains multiply orchestration chaos faster than capability growth. + ## Ready Signal The project can leave the current breakpoint when: diff --git a/docs/ARCH/11 - architecture_turnaround/13 - pre_multidomain_readiness_audit_2026-04-18.md b/docs/ARCH/11 - architecture_turnaround/13 - pre_multidomain_readiness_audit_2026-04-18.md new file mode 100644 index 0000000..8c76d7d --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/13 - pre_multidomain_readiness_audit_2026-04-18.md @@ -0,0 +1,170 @@ +# 13 - Pre-Multidomain Readiness Audit (2026-04-18) + +## Purpose + +This note answers one question directly: + +- are we already ready to expand into many new domains in parallel? + +The answer must stay architecture-first and brutally honest. + +## Executive Verdict + +Short version: + +- the project is no longer in the acute collapse state; +- the turnaround is real and already operational; +- but the system is still not ready for low-risk broad multi-domain expansion. + +Current verdict: + +- safe for continued hardening and controlled domain-by-domain expansion under replay gates; +- not yet safe for wide parallel multi-agent domain expansion. + +## What Is Already True + +The following claims are now supported by code plus live replay evidence: + +- phase7-phase11 mixed/manual replays are accepted on the repaired hot paths; +- continuity on validated inventory / VAT / counterparty / company-authority chains is materially stronger than before; +- user-facing meta answers are significantly cleaner and no longer dominated by technical garbage; +- the assistant no longer depends on the old ambient monolith behavior on the validated seams; +- the team now has a working replay-driven hardening loop instead of blind local patching. + +In practical terms: + +- we are moving out of danger; +- we are not yet on stable pre-expansion ground. + +## What Is Still Not Good Enough + +### 1. Continuity authority is improved, but still not singular + +The same active-context signal is still reconstructed in multiple places: + +- normalizer semantic hints; +- semantic overlay; +- transition policy; +- query/runtime guards; +- capability binding / answer-time anchor checks. + +This is much better than the old implicit monolith, but it still means: + +- the system relies on multiple synchronized interpretations of context instead of one final runtime authority object. + +### 2. Core orchestration remains too concentrated + +The main pressure centers are still heavy: + +- `assistantService.ts` +- `addressQueryService.ts` +- `answerComposer.ts` +- `decomposeStage.ts` +- `assistantTransitionPolicy.ts` + +This does not mean the extraction failed. + +It means the extraction is incomplete for the next scale step. + +### 3. Some fixes are still seam-specific rather than declarative + +Several repaired paths are now correct because explicit rules were added for real regressions. + +That is the right move during stabilization. + +But it also means: + +- the system still contains special-case authority at service/policy level that should later move into more declarative runtime contracts or registries. + +### 4. Acceptance breadth is still below the future blast radius + +Current replay evidence is strong on validated hot paths. + +It is not yet broad enough for the intended next stage: + +- many new domains; +- many new follow-up trees; +- multiple agents hardening in parallel. + +This is the single biggest reason not to declare the architecture expansion-ready yet. + +## Readiness Assessment + +### Safe right now + +- continue architectural hardening; +- continue replay-driven stabilization; +- onboard one new domain at a time under strict scenario acceptance; +- keep improving continuity authority and answer shaping. + +### Not safe right now + +- broad multi-domain rollout without stronger gates; +- parallel domain expansion that assumes the orchestration layer is already platform-grade; +- treating phase7-phase11 green status as proof that the general architecture is already robust enough for the next development level. + +## Required Before Next Development Level + +The system should not be considered ready for the next level until all of the following are true: + +1. `assistant_session_continuity_v1` is the real shared authority across route, transition, clarification, recap, and answer-shaping hot paths. +2. Saved-session acceptance is widened beyond the current repaired chains into a broader mixed replay pool. +3. Capability/meta handling is less service-special-case and more contract-driven. +4. `assistantService` pressure is reduced enough that new domains do not have to negotiate multiple partially overlapping owners. +5. Long exact answers feel human and business-first, not merely technically correct. + +## Recommended Next Execution Sequence + +### Pass 12. Continuity authority completion + +Goal: + +- reduce the number of places that reconstruct active context independently. + +Target: + +- transition / route / clarification should consume one continuity snapshot before making divergent decisions. + +### Pass 13. Wider saved-session acceptance pool + +Goal: + +- prove stability on multiple real user trajectories, not only the already repaired flagship chains. + +Target: + +- several saved sessions covering inventory, VAT, counterparty, payables/receivables, meta interrupts, and cross-domain pivots. + +### Pass 14. Human answer shaping cleanup + +Goal: + +- remove the remaining mechanical, template-heavy feel from long exact answers. + +Target: + +- product-quality business answers on already-correct truth paths. + +### Pass 15. Coordinator pressure reduction + +Goal: + +- make the architecture safer for future domain onboarding by shrinking control-plane overload. + +Target: + +- less policy/service glue concentrated in `assistantService.ts` and adjacent god-modules. + +## Final Statement + +The current architecture is no longer failing in the same way it failed during the regression breakpoint. + +That is a major win. + +But if we pretend this already equals multi-domain readiness, we will recreate the same class of project risk at a larger scale. + +The correct reading is: + +- collapse averted; +- stabilization real; +- expansion still gated. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index b3d9c7c..3da2dd5 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -29,12 +29,14 @@ This package answers the next question: 9. [09 - pre_expansion_cut_2026-04-17.md](./09%20-%20pre_expansion_cut_2026-04-17.md) 10. [10 - regression_breakpoint_analysis_2026-04-17.md](./10%20-%20regression_breakpoint_analysis_2026-04-17.md) 11. [11 - continuity_stabilization_plan_2026-04-17.md](./11%20-%20continuity_stabilization_plan_2026-04-17.md) +12. [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md) +13. [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md) -## Current Status Snapshot (2026-04-17) +## Current Status Snapshot (2026-04-18) This package is no longer planning-only. -It now documents a turnaround that is already operational in code but still inside a pre-expansion stabilization breakpoint: +It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, but still not ready for wide multi-domain expansion: - route, transition, boundary, meta, memory, and provider policy owners exist as separate modules; - exact-lane truth and coverage/evidence contracts exist as explicit runtime artifacts; @@ -43,18 +45,20 @@ It now documents a turnaround that is already operational in code but still insi Current honest status: -- turnaround implementation progress: `~88%` -- pre-expansion readiness: `~62%` -- graph snapshot after latest rebuild: `5312 nodes`, `11408 edges`, `136 communities` +- turnaround implementation progress: `~90%` +- exit-from-danger-zone readiness: `~78%` +- pre-multidomain readiness: `~58%` +- graph snapshot after latest rebuild: `5339 nodes`, `11476 edges`, `134 communities` - current breakpoint: - - mixed saved-session runtime still fails on continuity-critical edges; - - clarification can outrank restored business context; - - recap and user-facing packaging can remain smoother than the actual grounded thread. + - the validated hot paths are no longer structurally broken; + - but mixed continuity is still not governed by one fully central runtime authority; + - wider saved-session proof is still too narrow for low-risk multi-domain rollout; + - answer shaping is still heavier and more template-driven than the target product feel. - main remaining architectural pressure: - - no single authoritative continuity contract for live mixed sessions + - no single fully authoritative continuity contract consumed by all hot runtime owners - residual coordinator/legacy pressure inside `assistantService.ts` - central domain-intent pressure inside `resolveAddressIntent()` - - remaining answer-semantics pressure inside `composeStage.ts` + - remaining answer-semantics pressure inside `composeStage.ts` / `answerComposer.ts` For the detailed audit, current percentages, and remaining debt, read: @@ -62,6 +66,8 @@ For the detailed audit, current percentages, and remaining debt, read: - [09 - pre_expansion_cut_2026-04-17.md](./09%20-%20pre_expansion_cut_2026-04-17.md) - [10 - regression_breakpoint_analysis_2026-04-17.md](./10%20-%20regression_breakpoint_analysis_2026-04-17.md) - [11 - continuity_stabilization_plan_2026-04-17.md](./11%20-%20continuity_stabilization_plan_2026-04-17.md) +- [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md) +- [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md) ## Architectural Objects Of Planning @@ -91,6 +97,8 @@ Read in this order: 10. `09 - pre_expansion_cut_2026-04-17.md` 11. `10 - regression_breakpoint_analysis_2026-04-17.md` 12. `11 - continuity_stabilization_plan_2026-04-17.md` +13. `12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md` +14. `13 - pre_multidomain_readiness_audit_2026-04-18.md` ## Planning Rules @@ -110,12 +118,13 @@ and start being described as: - "a stateful exact-data assistant with explicit transition contracts and isolated truth gating." -As of `2026-04-17`, the project is already materially closer to the target description, but mixed-session continuity is still not governed by one runtime authority. +As of `2026-04-18`, the project is already materially closer to the target description and no longer in the same acute collapse state, but mixed-session continuity is still not governed by one runtime authority strongly enough to justify low-risk multi-domain expansion. The biggest remaining blockers are: - split continuity ownership across route / transition / recap / coordinator glue; +- saved-session acceptance still too narrow compared with the intended domain-expansion blast radius; - clarification precedence still too strong in mixed sessions; - residual `assistantService` overload; - central intent pressure in `resolveAddressIntent()`; -- remaining answer-semantics pressure in `composeStage.ts`. +- remaining answer-semantics pressure in `composeStage.ts` and `answerComposer.ts`. diff --git a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js index 6b2323a..365e332 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -5,6 +5,7 @@ exports.readAddressDebugFilters = readAddressDebugFilters; exports.readAddressDebugItem = readAddressDebugItem; exports.readAddressDebugOrganization = readAddressDebugOrganization; exports.readAddressDebugScopedDate = readAddressDebugScopedDate; +exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug; exports.isGroundedAddressDebug = isGroundedAddressDebug; exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot; function fallbackToNonEmptyString(value) { @@ -50,6 +51,53 @@ function readAddressDebugScopedDate(debug) { formatIsoDateForReply(rootFrameContext?.as_of_date) ?? formatIsoDateForReply(extractedFilters?.period_to)); } +function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) { + if (!debug || typeof debug !== "object") { + return null; + } + const rootFrameContext = toRecordObject(debug.address_root_frame_context); + const extractedFilters = readAddressDebugFilters(debug) ?? {}; + const detectedIntent = toNonEmptyString(debug.detected_intent); + const rootIntent = toNonEmptyString(rootFrameContext?.root_intent); + const effectiveIntent = rootIntent ?? detectedIntent; + if (effectiveIntent !== "inventory_on_hand_as_of_date") { + return null; + } + const rootFiltersCandidate = toRecordObject(rootFrameContext?.root_filters); + const filters = { + ...(rootFiltersCandidate ?? {}), + ...(rootFiltersCandidate ? {} : extractedFilters) + }; + if (!filters.organization) { + const organization = readAddressDebugOrganization(debug, toNonEmptyString); + if (organization) { + filters.organization = organization; + } + } + if (!filters.as_of_date) { + const scopedDate = formatIsoDateForReply(readAddressScopedIso(debug)); + if (scopedDate) { + const parts = scopedDate.split("."); + filters.as_of_date = `${parts[2]}-${parts[1]}-${parts[0]}`; + } + } + return { + intent: "inventory_on_hand_as_of_date", + filters, + anchorType: toNonEmptyString(rootFrameContext?.root_anchor_type) ?? toNonEmptyString(debug.anchor_type), + anchorValue: toNonEmptyString(rootFrameContext?.root_anchor_value) ?? + toNonEmptyString(debug.anchor_value_resolved) ?? + toNonEmptyString(debug.anchor_value_raw), + currentFrameKind: toNonEmptyString(rootFrameContext?.current_frame_kind) ?? "inventory_root" + }; +} +function readAddressScopedIso(debug) { + const extractedFilters = readAddressDebugFilters(debug); + const rootFrameContext = toRecordObject(debug?.address_root_frame_context); + return (fallbackToNonEmptyString(extractedFilters?.as_of_date) ?? + fallbackToNonEmptyString(rootFrameContext?.as_of_date) ?? + fallbackToNonEmptyString(extractedFilters?.period_to)); +} function isGroundedAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) { if (!debug || typeof debug !== "object") { return false; @@ -103,10 +151,13 @@ function resolveAssistantContinuitySnapshot(input) { } } const primaryDebug = lastGroundedItemAddressDebug ?? lastGroundedAddressDebug; + const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(lastGroundedInventoryAddressDebug, toNonEmptyString) ?? + buildInventoryRootFrameFromAddressDebug(lastGroundedAddressDebug, toNonEmptyString); return { lastGroundedAddressDebug, lastGroundedItemAddressDebug, lastGroundedInventoryAddressDebug, + inventoryRootFrame, activeItem: readAddressDebugItem(primaryDebug, toNonEmptyString), activeOrganization: readAddressDebugOrganization(primaryDebug, toNonEmptyString), activeScopedDate: readAddressDebugScopedDate(primaryDebug), diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index 7971a8a..14a1aed 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -1,7 +1,8 @@ "use strict"; -// @ts-nocheck Object.defineProperty(exports, "__esModule", { value: true }); exports.createAssistantTransitionPolicy = createAssistantTransitionPolicy; +// @ts-nocheck +const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy"); function createAssistantTransitionPolicy(deps) { function normalizeFollowupText(value) { return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е"); @@ -154,16 +155,6 @@ function createAssistantTransitionPolicy(deps) { } return null; } - function readAddressDebugItemHint(debug) { - if (!debug || typeof debug !== "object") { - return null; - } - const extractedFilters = debug.extracted_filters && typeof debug.extracted_filters === "object" ? debug.extracted_filters : null; - return (deps.toNonEmptyString(extractedFilters?.item) ?? - (deps.toNonEmptyString(debug.anchor_type) === "item" - ? deps.toNonEmptyString(debug.anchor_value_resolved) ?? deps.toNonEmptyString(debug.anchor_value_raw) - : null)); - } function findRecentInventoryPurchaseProvenanceItem(items, itemHint = null) { const normalizedItemHint = deps.toNonEmptyString(itemHint); for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) { @@ -178,7 +169,7 @@ function createAssistantTransitionPolicy(deps) { if (!normalizedItemHint) { return item; } - const candidateItem = readAddressDebugItemHint(debug); + const candidateItem = (0, assistantContinuityPolicy_1.readAddressDebugItem)(debug, deps.toNonEmptyString); if (candidateItem && candidateItem === normalizedItemHint) { return item; } @@ -340,6 +331,10 @@ function createAssistantTransitionPolicy(deps) { ? latestAddressItem : findRecentUsableAddressAssistantItem(items)) ?? latestAddressItem; const previousAddressDebug = previousAddressItem?.debug ?? null; + const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ + sessionItems: items, + toNonEmptyString: deps.toNonEmptyString + }); const lastOrganizationClarificationDebug = deps.findLastOrganizationClarificationAddressDebug(items); const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) ? deps.mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates) @@ -591,7 +586,9 @@ function createAssistantTransitionPolicy(deps) { const selectedObjectRetargetIntent = hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate ? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage) : null; - let inventoryRootFrame = deps.findRecentInventoryRootFrame(items); + let inventoryRootFrame = deps.findRecentInventoryRootFrame(items) ?? + continuitySnapshot.inventoryRootFrame ?? + (0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString); if (inventoryRootFrame && navigationOrganization && !deps.toNonEmptyString(inventoryRootFrame.filters?.organization)) { inventoryRootFrame = { ...inventoryRootFrame, @@ -653,6 +650,9 @@ function createAssistantTransitionPolicy(deps) { previousFilters.organization = historicalOrganization; } } + if (!deps.toNonEmptyString(previousFilters.organization) && continuitySnapshot.activeOrganization) { + previousFilters.organization = continuitySnapshot.activeOrganization; + } if (!deps.toNonEmptyString(previousFilters.organization) && navigationOrganization) { previousFilters.organization = navigationOrganization; } @@ -664,7 +664,7 @@ function createAssistantTransitionPolicy(deps) { deps.toNonEmptyString(previousAddressDebug?.detected_intent) === "inventory_purchase_provenance_for_item" ? previousAddressItem : findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ?? - readAddressDebugItemHint(previousAddressDebug) ?? + (0, assistantContinuityPolicy_1.readAddressDebugItem)(previousAddressDebug, deps.toNonEmptyString) ?? deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem; const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState); if (purchaseBridgeWindow) { @@ -687,16 +687,31 @@ function createAssistantTransitionPolicy(deps) { deps.toNonEmptyString(navigationDateScope?.as_of_date)) { previousFilters.as_of_date = deps.toNonEmptyString(navigationDateScope?.as_of_date); } + if (shouldBackfillPreviousDateScopeFromNavigation && + !deps.toNonEmptyString(previousFilters.as_of_date) && + (0, assistantContinuityPolicy_1.readAddressDebugFilters)(continuitySnapshot.lastGroundedAddressDebug)?.as_of_date) { + previousFilters.as_of_date = deps.toNonEmptyString((0, assistantContinuityPolicy_1.readAddressDebugFilters)(continuitySnapshot.lastGroundedAddressDebug)?.as_of_date); + } if (shouldBackfillPreviousDateScopeFromNavigation && !deps.toNonEmptyString(previousFilters.period_from) && deps.toNonEmptyString(navigationDateScope?.period_from)) { previousFilters.period_from = deps.toNonEmptyString(navigationDateScope?.period_from); } + if (shouldBackfillPreviousDateScopeFromNavigation && + !deps.toNonEmptyString(previousFilters.period_from) && + (0, assistantContinuityPolicy_1.readAddressDebugFilters)(continuitySnapshot.lastGroundedAddressDebug)?.period_from) { + previousFilters.period_from = deps.toNonEmptyString((0, assistantContinuityPolicy_1.readAddressDebugFilters)(continuitySnapshot.lastGroundedAddressDebug)?.period_from); + } if (shouldBackfillPreviousDateScopeFromNavigation && !deps.toNonEmptyString(previousFilters.period_to) && deps.toNonEmptyString(navigationDateScope?.period_to)) { previousFilters.period_to = deps.toNonEmptyString(navigationDateScope?.period_to); } + if (shouldBackfillPreviousDateScopeFromNavigation && + !deps.toNonEmptyString(previousFilters.period_to) && + (0, assistantContinuityPolicy_1.readAddressDebugFilters)(continuitySnapshot.lastGroundedAddressDebug)?.period_to) { + previousFilters.period_to = deps.toNonEmptyString((0, assistantContinuityPolicy_1.readAddressDebugFilters)(continuitySnapshot.lastGroundedAddressDebug)?.period_to); + } const rootContextOnlyPivot = Boolean((deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") && deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage) && !inventoryPurchaseDateVatBridge); @@ -767,6 +782,7 @@ function createAssistantTransitionPolicy(deps) { hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate)) { const selectedObjectLabel = (navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null) ?? + continuitySnapshot.activeItem ?? extractSelectedObjectLabel(userMessage) ?? (deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null); if (selectedObjectLabel) { diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index 383ad35..f99474f 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -7,6 +7,13 @@ export interface AssistantContinuitySnapshot { lastGroundedAddressDebug: Record | null; lastGroundedItemAddressDebug: Record | null; lastGroundedInventoryAddressDebug: Record | null; + inventoryRootFrame: { + intent: string; + filters: Record; + anchorType: string | null; + anchorValue: string | null; + currentFrameKind: string; + } | null; activeItem: string | null; activeOrganization: string | null; activeScopedDate: string | null; @@ -75,6 +82,69 @@ export function readAddressDebugScopedDate(debug: Record | null ); } +export function buildInventoryRootFrameFromAddressDebug( + debug: Record | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): { + intent: string; + filters: Record; + anchorType: string | null; + anchorValue: string | null; + currentFrameKind: string; +} | null { + if (!debug || typeof debug !== "object") { + return null; + } + const rootFrameContext = toRecordObject(debug.address_root_frame_context); + const extractedFilters = readAddressDebugFilters(debug) ?? {}; + const detectedIntent = toNonEmptyString(debug.detected_intent); + const rootIntent = toNonEmptyString(rootFrameContext?.root_intent); + const effectiveIntent = rootIntent ?? detectedIntent; + if (effectiveIntent !== "inventory_on_hand_as_of_date") { + return null; + } + + const rootFiltersCandidate = toRecordObject(rootFrameContext?.root_filters); + const filters = { + ...(rootFiltersCandidate ?? {}), + ...(rootFiltersCandidate ? {} : extractedFilters) + }; + if (!filters.organization) { + const organization = readAddressDebugOrganization(debug, toNonEmptyString); + if (organization) { + filters.organization = organization; + } + } + if (!filters.as_of_date) { + const scopedDate = formatIsoDateForReply(readAddressScopedIso(debug)); + if (scopedDate) { + const parts = scopedDate.split("."); + filters.as_of_date = `${parts[2]}-${parts[1]}-${parts[0]}`; + } + } + + return { + intent: "inventory_on_hand_as_of_date", + filters, + anchorType: toNonEmptyString(rootFrameContext?.root_anchor_type) ?? toNonEmptyString(debug.anchor_type), + anchorValue: + toNonEmptyString(rootFrameContext?.root_anchor_value) ?? + toNonEmptyString(debug.anchor_value_resolved) ?? + toNonEmptyString(debug.anchor_value_raw), + currentFrameKind: toNonEmptyString(rootFrameContext?.current_frame_kind) ?? "inventory_root" + }; +} + +function readAddressScopedIso(debug: Record | null): string | null { + const extractedFilters = readAddressDebugFilters(debug); + const rootFrameContext = toRecordObject(debug?.address_root_frame_context); + return ( + fallbackToNonEmptyString(extractedFilters?.as_of_date) ?? + fallbackToNonEmptyString(rootFrameContext?.as_of_date) ?? + fallbackToNonEmptyString(extractedFilters?.period_to) + ); +} + export function isGroundedAddressDebug( debug: Record | null, toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString @@ -142,10 +212,14 @@ export function resolveAssistantContinuitySnapshot( } const primaryDebug = lastGroundedItemAddressDebug ?? lastGroundedAddressDebug; + const inventoryRootFrame = + buildInventoryRootFrameFromAddressDebug(lastGroundedInventoryAddressDebug, toNonEmptyString) ?? + buildInventoryRootFrameFromAddressDebug(lastGroundedAddressDebug, toNonEmptyString); return { lastGroundedAddressDebug, lastGroundedItemAddressDebug, lastGroundedInventoryAddressDebug, + inventoryRootFrame, activeItem: readAddressDebugItem(primaryDebug, toNonEmptyString), activeOrganization: readAddressDebugOrganization(primaryDebug, toNonEmptyString), activeScopedDate: readAddressDebugScopedDate(primaryDebug), diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 2f43fb9..4f4e8b1 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -1,4 +1,10 @@ // @ts-nocheck +import { + buildInventoryRootFrameFromAddressDebug, + readAddressDebugFilters, + readAddressDebugItem, + resolveAssistantContinuitySnapshot +} from "./assistantContinuityPolicy"; export function createAssistantTransitionPolicy(deps) { function normalizeFollowupText(value) { @@ -186,20 +192,6 @@ export function createAssistantTransitionPolicy(deps) { return null; } - function readAddressDebugItemHint(debug) { - if (!debug || typeof debug !== "object") { - return null; - } - const extractedFilters = - debug.extracted_filters && typeof debug.extracted_filters === "object" ? debug.extracted_filters : null; - return ( - deps.toNonEmptyString(extractedFilters?.item) ?? - (deps.toNonEmptyString(debug.anchor_type) === "item" - ? deps.toNonEmptyString(debug.anchor_value_resolved) ?? deps.toNonEmptyString(debug.anchor_value_raw) - : null) - ); - } - function findRecentInventoryPurchaseProvenanceItem(items, itemHint = null) { const normalizedItemHint = deps.toNonEmptyString(itemHint); for (let index = Array.isArray(items) ? items.length - 1 : -1; index >= 0; index -= 1) { @@ -214,7 +206,7 @@ export function createAssistantTransitionPolicy(deps) { if (!normalizedItemHint) { return item; } - const candidateItem = readAddressDebugItemHint(debug); + const candidateItem = readAddressDebugItem(debug, deps.toNonEmptyString); if (candidateItem && candidateItem === normalizedItemHint) { return item; } @@ -421,6 +413,10 @@ export function createAssistantTransitionPolicy(deps) { ? latestAddressItem : findRecentUsableAddressAssistantItem(items)) ?? latestAddressItem; const previousAddressDebug = previousAddressItem?.debug ?? null; + const continuitySnapshot = resolveAssistantContinuitySnapshot({ + sessionItems: items, + toNonEmptyString: deps.toNonEmptyString + }); const lastOrganizationClarificationDebug = deps.findLastOrganizationClarificationAddressDebug(items); const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) ? deps.mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates) @@ -727,7 +723,10 @@ export function createAssistantTransitionPolicy(deps) { hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate ? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage) : null; - let inventoryRootFrame = deps.findRecentInventoryRootFrame(items); + let inventoryRootFrame = + deps.findRecentInventoryRootFrame(items) ?? + continuitySnapshot.inventoryRootFrame ?? + buildInventoryRootFrameFromAddressDebug(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString); if (inventoryRootFrame && navigationOrganization && !deps.toNonEmptyString(inventoryRootFrame.filters?.organization)) { inventoryRootFrame = { ...inventoryRootFrame, @@ -794,6 +793,9 @@ export function createAssistantTransitionPolicy(deps) { previousFilters.organization = historicalOrganization; } } + if (!deps.toNonEmptyString(previousFilters.organization) && continuitySnapshot.activeOrganization) { + previousFilters.organization = continuitySnapshot.activeOrganization; + } if (!deps.toNonEmptyString(previousFilters.organization) && navigationOrganization) { previousFilters.organization = navigationOrganization; } @@ -808,7 +810,7 @@ export function createAssistantTransitionPolicy(deps) { : findRecentInventoryPurchaseProvenanceItem( items, deps.toNonEmptyString(navigationFocusObjectLabel) ?? - readAddressDebugItemHint(previousAddressDebug) ?? + readAddressDebugItem(previousAddressDebug, deps.toNonEmptyString) ?? deps.toNonEmptyString(previousFilters.item) ) ?? previousAddressItem; const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState); @@ -835,6 +837,15 @@ export function createAssistantTransitionPolicy(deps) { ) { previousFilters.as_of_date = deps.toNonEmptyString(navigationDateScope?.as_of_date); } + if ( + shouldBackfillPreviousDateScopeFromNavigation && + !deps.toNonEmptyString(previousFilters.as_of_date) && + readAddressDebugFilters(continuitySnapshot.lastGroundedAddressDebug)?.as_of_date + ) { + previousFilters.as_of_date = deps.toNonEmptyString( + readAddressDebugFilters(continuitySnapshot.lastGroundedAddressDebug)?.as_of_date + ); + } if ( shouldBackfillPreviousDateScopeFromNavigation && !deps.toNonEmptyString(previousFilters.period_from) && @@ -842,6 +853,15 @@ export function createAssistantTransitionPolicy(deps) { ) { previousFilters.period_from = deps.toNonEmptyString(navigationDateScope?.period_from); } + if ( + shouldBackfillPreviousDateScopeFromNavigation && + !deps.toNonEmptyString(previousFilters.period_from) && + readAddressDebugFilters(continuitySnapshot.lastGroundedAddressDebug)?.period_from + ) { + previousFilters.period_from = deps.toNonEmptyString( + readAddressDebugFilters(continuitySnapshot.lastGroundedAddressDebug)?.period_from + ); + } if ( shouldBackfillPreviousDateScopeFromNavigation && !deps.toNonEmptyString(previousFilters.period_to) && @@ -849,6 +869,15 @@ export function createAssistantTransitionPolicy(deps) { ) { previousFilters.period_to = deps.toNonEmptyString(navigationDateScope?.period_to); } + if ( + shouldBackfillPreviousDateScopeFromNavigation && + !deps.toNonEmptyString(previousFilters.period_to) && + readAddressDebugFilters(continuitySnapshot.lastGroundedAddressDebug)?.period_to + ) { + previousFilters.period_to = deps.toNonEmptyString( + readAddressDebugFilters(continuitySnapshot.lastGroundedAddressDebug)?.period_to + ); + } const rootContextOnlyPivot = Boolean( (deps.isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") && deps.hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage) && @@ -935,6 +964,7 @@ export function createAssistantTransitionPolicy(deps) { ) { const selectedObjectLabel = (navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null) ?? + continuitySnapshot.activeItem ?? extractSelectedObjectLabel(userMessage) ?? (deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null); if (selectedObjectLabel) {