Архитектура: централизовать inventory root-frame state и root-scoped carryover в continuity policy
This commit is contained in:
parent
c9730c986a
commit
46dfef6fb6
|
|
@ -422,6 +422,12 @@ Still open after the accepted phase12 replay:
|
||||||
- targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, including explicit regression coverage for `inventory_purchase_documents_for_item -> inventory_on_hand_as_of_date` carryover where `root_filters` must override a stale drilldown date;
|
- targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, including explicit regression coverage for `inventory_purchase_documents_for_item -> inventory_on_hand_as_of_date` carryover where `root_filters` must override a stale drilldown date;
|
||||||
- this pass reduces one more hidden state-reconstruction fork between the continuity layer and transition glue without introducing case-specific routing;
|
- this pass reduces one more hidden state-reconstruction fork between the continuity layer and transition glue without introducing case-specific routing;
|
||||||
- a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` stayed semantically clean on the repaired carryover path and failed only on the already-known time-unstable `today` expectations (`2026-04-18` vs `2026-04-19`) in `inventory_root_today`, `payables_today`, and `receivables_mirror_today`.
|
- a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` stayed semantically clean on the repaired carryover path and failed only on the already-known time-unstable `today` expectations (`2026-04-18` vs `2026-04-19`) in `inventory_root_today`, `payables_today`, and `receivables_mirror_today`.
|
||||||
|
- the next continuity-authority pass now centralizes one more shared inventory root-frame seam that used to be split across `assistantService` and `assistantTransitionPolicy`:
|
||||||
|
- continuity now owns `hydrateInventoryRootFrameState(...)`, which fills missing organization/date scope into `inventoryRootFrame` and computes `currentFrameKind` from the same shared state object instead of rebuilding both pieces locally inside transition glue;
|
||||||
|
- continuity now also owns `buildRootScopedCarryoverFilters(...)`, so root-scoped filter precedence no longer lives as a separate service-local helper and tests no longer need a legacy re-export from `assistantService`;
|
||||||
|
- this matters because `inventoryRootFrame`, `current_frame_kind`, and `root-scoped` filter precedence now converge through one authority layer before `root_context_only` pivots are decided, which reduces another hidden chance for state drift when new domains or new follow-up families are added;
|
||||||
|
- targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, with explicit coverage for root-frame hydration from navigation scope and for previous-date precedence over a stale inventory root frame;
|
||||||
|
- a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` remained semantically stable on all repaired continuity paths and again failed only on the already-known date-sensitive `today` expectations, not on the new shared root-frame state owner.
|
||||||
|
|
||||||
## Next Execution Slice (2026-04-18)
|
## Next Execution Slice (2026-04-18)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope;
|
||||||
exports.resolveAddressDebugAnchorContext = resolveAddressDebugAnchorContext;
|
exports.resolveAddressDebugAnchorContext = resolveAddressDebugAnchorContext;
|
||||||
exports.resolveAddressDebugContextFacts = resolveAddressDebugContextFacts;
|
exports.resolveAddressDebugContextFacts = resolveAddressDebugContextFacts;
|
||||||
exports.resolveAddressDebugCarryoverFilters = resolveAddressDebugCarryoverFilters;
|
exports.resolveAddressDebugCarryoverFilters = resolveAddressDebugCarryoverFilters;
|
||||||
|
exports.hydrateInventoryRootFrameState = hydrateInventoryRootFrameState;
|
||||||
|
exports.buildRootScopedCarryoverFilters = buildRootScopedCarryoverFilters;
|
||||||
exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug;
|
exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug;
|
||||||
exports.isGroundedAddressDebug = isGroundedAddressDebug;
|
exports.isGroundedAddressDebug = isGroundedAddressDebug;
|
||||||
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
|
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
|
||||||
|
|
@ -153,6 +155,84 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT
|
||||||
}
|
}
|
||||||
return nextFilters;
|
return nextFilters;
|
||||||
}
|
}
|
||||||
|
function hydrateInventoryRootFrameState(inventoryRootFrame, sourceIntent, navigationOrganization, navigationDateScope, toNonEmptyString = fallbackToNonEmptyString, isInventoryDrilldownFrameIntent = () => false, isInventoryRootFrameIntent = () => false) {
|
||||||
|
if (!inventoryRootFrame) {
|
||||||
|
return { inventoryRootFrame: null, currentFrameKind: null };
|
||||||
|
}
|
||||||
|
let hydratedRootFrame = {
|
||||||
|
...inventoryRootFrame,
|
||||||
|
filters: {
|
||||||
|
...(inventoryRootFrame.filters ?? {})
|
||||||
|
},
|
||||||
|
currentFrameKind: toNonEmptyString(inventoryRootFrame.currentFrameKind) ?? null
|
||||||
|
};
|
||||||
|
if (navigationOrganization && !toNonEmptyString(hydratedRootFrame.filters?.organization)) {
|
||||||
|
hydratedRootFrame = {
|
||||||
|
...hydratedRootFrame,
|
||||||
|
filters: {
|
||||||
|
...(hydratedRootFrame.filters ?? {}),
|
||||||
|
organization: navigationOrganization
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (navigationDateScope) {
|
||||||
|
hydratedRootFrame = {
|
||||||
|
...hydratedRootFrame,
|
||||||
|
filters: {
|
||||||
|
...(hydratedRootFrame.filters ?? {}),
|
||||||
|
as_of_date: toNonEmptyString(hydratedRootFrame.filters?.as_of_date) ??
|
||||||
|
toNonEmptyString(navigationDateScope.as_of_date) ??
|
||||||
|
undefined,
|
||||||
|
period_from: toNonEmptyString(hydratedRootFrame.filters?.period_from) ??
|
||||||
|
toNonEmptyString(navigationDateScope.period_from) ??
|
||||||
|
undefined,
|
||||||
|
period_to: toNonEmptyString(hydratedRootFrame.filters?.period_to) ??
|
||||||
|
toNonEmptyString(navigationDateScope.period_to) ??
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const currentFrameKind = toNonEmptyString(hydratedRootFrame.currentFrameKind) ??
|
||||||
|
(isInventoryDrilldownFrameIntent(sourceIntent)
|
||||||
|
? "inventory_drilldown"
|
||||||
|
: isInventoryRootFrameIntent(sourceIntent)
|
||||||
|
? "inventory_root"
|
||||||
|
: "generic");
|
||||||
|
return {
|
||||||
|
inventoryRootFrame: {
|
||||||
|
...hydratedRootFrame,
|
||||||
|
currentFrameKind
|
||||||
|
},
|
||||||
|
currentFrameKind
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
|
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||||||
|
? inventoryRootFrame.filters
|
||||||
|
: {};
|
||||||
|
const nextFilters = {};
|
||||||
|
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
||||||
|
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
||||||
|
const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date);
|
||||||
|
const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from);
|
||||||
|
const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to);
|
||||||
|
if (organization) {
|
||||||
|
nextFilters.organization = organization;
|
||||||
|
}
|
||||||
|
if (warehouse) {
|
||||||
|
nextFilters.warehouse = warehouse;
|
||||||
|
}
|
||||||
|
if (asOfDate) {
|
||||||
|
nextFilters.as_of_date = asOfDate;
|
||||||
|
}
|
||||||
|
if (periodFrom) {
|
||||||
|
nextFilters.period_from = periodFrom;
|
||||||
|
}
|
||||||
|
if (periodTo) {
|
||||||
|
nextFilters.period_to = periodTo;
|
||||||
|
}
|
||||||
|
return nextFilters;
|
||||||
|
}
|
||||||
function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
if (!debug || typeof debug !== "object") {
|
if (!debug || typeof debug !== "object") {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ exports.evaluateCoverageForTests = evaluateCoverageForTests;
|
||||||
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
|
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
|
||||||
exports.resolveAssistantOrchestrationDecision = resolveAssistantOrchestrationDecision;
|
exports.resolveAssistantOrchestrationDecision = resolveAssistantOrchestrationDecision;
|
||||||
exports.resolveSessionOrganizationScopeContextForTests = resolveSessionOrganizationScopeContextForTests;
|
exports.resolveSessionOrganizationScopeContextForTests = resolveSessionOrganizationScopeContextForTests;
|
||||||
exports.buildRootScopedCarryoverFiltersForTests = buildRootScopedCarryoverFiltersForTests;
|
|
||||||
exports.extractOrganizationFactsFromRowsForTests = extractOrganizationFactsFromRowsForTests;
|
exports.extractOrganizationFactsFromRowsForTests = extractOrganizationFactsFromRowsForTests;
|
||||||
exports.resolveOrganizationNamesByRefsForTests = resolveOrganizationNamesByRefsForTests;
|
exports.resolveOrganizationNamesByRefsForTests = resolveOrganizationNamesByRefsForTests;
|
||||||
exports.resolveLivingAssistantModeDecision = resolveLivingAssistantModeDecision;
|
exports.resolveLivingAssistantModeDecision = resolveLivingAssistantModeDecision;
|
||||||
|
|
@ -2758,31 +2757,7 @@ function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMes
|
||||||
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
||||||
}
|
}
|
||||||
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
||||||
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
return (0, assistantContinuityPolicy_1.buildRootScopedCarryoverFilters)(previousFilters, inventoryRootFrame, toNonEmptyString);
|
||||||
? inventoryRootFrame.filters
|
|
||||||
: {};
|
|
||||||
const nextFilters = {};
|
|
||||||
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
|
||||||
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
|
||||||
const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date);
|
|
||||||
const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from);
|
|
||||||
const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to);
|
|
||||||
if (organization) {
|
|
||||||
nextFilters.organization = organization;
|
|
||||||
}
|
|
||||||
if (warehouse) {
|
|
||||||
nextFilters.warehouse = warehouse;
|
|
||||||
}
|
|
||||||
if (asOfDate) {
|
|
||||||
nextFilters.as_of_date = asOfDate;
|
|
||||||
}
|
|
||||||
if (periodFrom) {
|
|
||||||
nextFilters.period_from = periodFrom;
|
|
||||||
}
|
|
||||||
if (periodTo) {
|
|
||||||
nextFilters.period_to = periodTo;
|
|
||||||
}
|
|
||||||
return nextFilters;
|
|
||||||
}
|
}
|
||||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
|
@ -4573,9 +4548,6 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization
|
||||||
function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
|
function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
|
||||||
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
|
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
|
||||||
}
|
}
|
||||||
function buildRootScopedCarryoverFiltersForTests(previousFilters, inventoryRootFrame) {
|
|
||||||
return buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
|
||||||
}
|
|
||||||
function normalizeGuidValue(value) {
|
function normalizeGuidValue(value) {
|
||||||
const source = normalizeScopeLabel(value);
|
const source = normalizeScopeLabel(value);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
|
|
|
||||||
|
|
@ -589,42 +589,10 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const selectedObjectRetargetIntent = hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate
|
const selectedObjectRetargetIntent = hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate
|
||||||
? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage)
|
? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage)
|
||||||
: null;
|
: null;
|
||||||
let inventoryRootFrame = deps.findRecentInventoryRootFrame(items) ??
|
const inventoryRootFrameCandidate = deps.findRecentInventoryRootFrame(items) ??
|
||||||
continuitySnapshot.inventoryRootFrame ??
|
continuitySnapshot.inventoryRootFrame ??
|
||||||
(0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString);
|
(0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString);
|
||||||
if (inventoryRootFrame && navigationOrganization && !deps.toNonEmptyString(inventoryRootFrame.filters?.organization)) {
|
let { inventoryRootFrame, currentFrameKind } = (0, assistantContinuityPolicy_1.hydrateInventoryRootFrameState)(inventoryRootFrameCandidate, sourceIntent, navigationOrganization, navigationDateScope, deps.toNonEmptyString, deps.isInventoryDrilldownFrameIntent, deps.isInventoryRootFrameIntent);
|
||||||
inventoryRootFrame = {
|
|
||||||
...inventoryRootFrame,
|
|
||||||
filters: {
|
|
||||||
...(inventoryRootFrame.filters ?? {}),
|
|
||||||
organization: navigationOrganization
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (inventoryRootFrame && navigationDateScope) {
|
|
||||||
inventoryRootFrame = {
|
|
||||||
...inventoryRootFrame,
|
|
||||||
filters: {
|
|
||||||
...(inventoryRootFrame.filters ?? {}),
|
|
||||||
as_of_date: deps.toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ??
|
|
||||||
deps.toNonEmptyString(navigationDateScope.as_of_date) ??
|
|
||||||
undefined,
|
|
||||||
period_from: deps.toNonEmptyString(inventoryRootFrame.filters?.period_from) ??
|
|
||||||
deps.toNonEmptyString(navigationDateScope.period_from) ??
|
|
||||||
undefined,
|
|
||||||
period_to: deps.toNonEmptyString(inventoryRootFrame.filters?.period_to) ??
|
|
||||||
deps.toNonEmptyString(navigationDateScope.period_to) ??
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let currentFrameKind = inventoryRootFrame
|
|
||||||
? deps.isInventoryDrilldownFrameIntent(sourceIntent)
|
|
||||||
? "inventory_drilldown"
|
|
||||||
: deps.isInventoryRootFrameIntent(sourceIntent)
|
|
||||||
? "inventory_root"
|
|
||||||
: "generic"
|
|
||||||
: null;
|
|
||||||
let resolvedCounterpartyFromDisplay = false;
|
let resolvedCounterpartyFromDisplay = false;
|
||||||
let displayedEntityTargetIntent = null;
|
let displayedEntityTargetIntent = null;
|
||||||
let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(previousAddressDebug, deps.toNonEmptyString);
|
let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(previousAddressDebug, deps.toNonEmptyString);
|
||||||
|
|
@ -745,7 +713,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
previousIntent = null;
|
previousIntent = null;
|
||||||
previousAnchorType = null;
|
previousAnchorType = null;
|
||||||
previousAnchor = null;
|
previousAnchor = null;
|
||||||
previousFilters = deps.buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
previousFilters = (0, assistantContinuityPolicy_1.buildRootScopedCarryoverFilters)(previousFilters, inventoryRootFrame, deps.toNonEmptyString);
|
||||||
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
||||||
followupSelectionMode = "carry_root_context";
|
followupSelectionMode = "carry_root_context";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ export interface AssistantAddressDebugTemporalScope {
|
||||||
periodTo: string | null;
|
periodTo: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssistantNavigationDateScope {
|
||||||
|
as_of_date?: unknown;
|
||||||
|
period_from?: unknown;
|
||||||
|
period_to?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssistantAddressDebugAnchorContext {
|
export interface AssistantAddressDebugAnchorContext {
|
||||||
anchorType: string | null;
|
anchorType: string | null;
|
||||||
anchorValue: string | null;
|
anchorValue: string | null;
|
||||||
|
|
@ -244,6 +250,125 @@ export function resolveAddressDebugCarryoverFilters(
|
||||||
return nextFilters;
|
return nextFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hydrateInventoryRootFrameState(
|
||||||
|
inventoryRootFrame: {
|
||||||
|
intent: string;
|
||||||
|
filters: Record<string, unknown>;
|
||||||
|
anchorType: string | null;
|
||||||
|
anchorValue: string | null;
|
||||||
|
currentFrameKind?: string | null;
|
||||||
|
} | null,
|
||||||
|
sourceIntent: unknown,
|
||||||
|
navigationOrganization: unknown,
|
||||||
|
navigationDateScope: AssistantNavigationDateScope | null,
|
||||||
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString,
|
||||||
|
isInventoryDrilldownFrameIntent: (intent: unknown) => boolean = () => false,
|
||||||
|
isInventoryRootFrameIntent: (intent: unknown) => boolean = () => false
|
||||||
|
): {
|
||||||
|
inventoryRootFrame: {
|
||||||
|
intent: string;
|
||||||
|
filters: Record<string, unknown>;
|
||||||
|
anchorType: string | null;
|
||||||
|
anchorValue: string | null;
|
||||||
|
currentFrameKind: string;
|
||||||
|
} | null;
|
||||||
|
currentFrameKind: string | null;
|
||||||
|
} {
|
||||||
|
if (!inventoryRootFrame) {
|
||||||
|
return { inventoryRootFrame: null, currentFrameKind: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let hydratedRootFrame = {
|
||||||
|
...inventoryRootFrame,
|
||||||
|
filters: {
|
||||||
|
...(inventoryRootFrame.filters ?? {})
|
||||||
|
},
|
||||||
|
currentFrameKind: toNonEmptyString(inventoryRootFrame.currentFrameKind) ?? null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (navigationOrganization && !toNonEmptyString(hydratedRootFrame.filters?.organization)) {
|
||||||
|
hydratedRootFrame = {
|
||||||
|
...hydratedRootFrame,
|
||||||
|
filters: {
|
||||||
|
...(hydratedRootFrame.filters ?? {}),
|
||||||
|
organization: navigationOrganization
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigationDateScope) {
|
||||||
|
hydratedRootFrame = {
|
||||||
|
...hydratedRootFrame,
|
||||||
|
filters: {
|
||||||
|
...(hydratedRootFrame.filters ?? {}),
|
||||||
|
as_of_date:
|
||||||
|
toNonEmptyString(hydratedRootFrame.filters?.as_of_date) ??
|
||||||
|
toNonEmptyString(navigationDateScope.as_of_date) ??
|
||||||
|
undefined,
|
||||||
|
period_from:
|
||||||
|
toNonEmptyString(hydratedRootFrame.filters?.period_from) ??
|
||||||
|
toNonEmptyString(navigationDateScope.period_from) ??
|
||||||
|
undefined,
|
||||||
|
period_to:
|
||||||
|
toNonEmptyString(hydratedRootFrame.filters?.period_to) ??
|
||||||
|
toNonEmptyString(navigationDateScope.period_to) ??
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFrameKind =
|
||||||
|
toNonEmptyString(hydratedRootFrame.currentFrameKind) ??
|
||||||
|
(isInventoryDrilldownFrameIntent(sourceIntent)
|
||||||
|
? "inventory_drilldown"
|
||||||
|
: isInventoryRootFrameIntent(sourceIntent)
|
||||||
|
? "inventory_root"
|
||||||
|
: "generic");
|
||||||
|
|
||||||
|
return {
|
||||||
|
inventoryRootFrame: {
|
||||||
|
...hydratedRootFrame,
|
||||||
|
currentFrameKind
|
||||||
|
},
|
||||||
|
currentFrameKind
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRootScopedCarryoverFilters(
|
||||||
|
previousFilters: Record<string, unknown> | null,
|
||||||
|
inventoryRootFrame: {
|
||||||
|
filters?: Record<string, unknown> | null;
|
||||||
|
} | null,
|
||||||
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const candidateFilters =
|
||||||
|
inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||||||
|
? inventoryRootFrame.filters
|
||||||
|
: {};
|
||||||
|
const nextFilters: Record<string, unknown> = {};
|
||||||
|
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
||||||
|
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
||||||
|
const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date);
|
||||||
|
const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from);
|
||||||
|
const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to);
|
||||||
|
if (organization) {
|
||||||
|
nextFilters.organization = organization;
|
||||||
|
}
|
||||||
|
if (warehouse) {
|
||||||
|
nextFilters.warehouse = warehouse;
|
||||||
|
}
|
||||||
|
if (asOfDate) {
|
||||||
|
nextFilters.as_of_date = asOfDate;
|
||||||
|
}
|
||||||
|
if (periodFrom) {
|
||||||
|
nextFilters.period_from = periodFrom;
|
||||||
|
}
|
||||||
|
if (periodTo) {
|
||||||
|
nextFilters.period_to = periodTo;
|
||||||
|
}
|
||||||
|
return nextFilters;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildInventoryRootFrameFromAddressDebug(
|
export function buildInventoryRootFrameFromAddressDebug(
|
||||||
debug: Record<string, unknown> | null,
|
debug: Record<string, unknown> | null,
|
||||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||||
|
|
|
||||||
|
|
@ -2713,31 +2713,7 @@ function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMes
|
||||||
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
||||||
}
|
}
|
||||||
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
||||||
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
return (0, assistantContinuityPolicy_1.buildRootScopedCarryoverFilters)(previousFilters, inventoryRootFrame, toNonEmptyString);
|
||||||
? inventoryRootFrame.filters
|
|
||||||
: {};
|
|
||||||
const nextFilters = {};
|
|
||||||
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
|
||||||
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
|
||||||
const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date);
|
|
||||||
const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from);
|
|
||||||
const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to);
|
|
||||||
if (organization) {
|
|
||||||
nextFilters.organization = organization;
|
|
||||||
}
|
|
||||||
if (warehouse) {
|
|
||||||
nextFilters.warehouse = warehouse;
|
|
||||||
}
|
|
||||||
if (asOfDate) {
|
|
||||||
nextFilters.as_of_date = asOfDate;
|
|
||||||
}
|
|
||||||
if (periodFrom) {
|
|
||||||
nextFilters.period_from = periodFrom;
|
|
||||||
}
|
|
||||||
if (periodTo) {
|
|
||||||
nextFilters.period_to = periodTo;
|
|
||||||
}
|
|
||||||
return nextFilters;
|
|
||||||
}
|
}
|
||||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
|
|
@ -4529,9 +4505,6 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization
|
||||||
export function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
|
export function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
|
||||||
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
|
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
|
||||||
}
|
}
|
||||||
export function buildRootScopedCarryoverFiltersForTests(previousFilters, inventoryRootFrame) {
|
|
||||||
return buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
|
||||||
}
|
|
||||||
function normalizeGuidValue(value) {
|
function normalizeGuidValue(value) {
|
||||||
const source = normalizeScopeLabel(value);
|
const source = normalizeScopeLabel(value);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import {
|
import {
|
||||||
|
buildRootScopedCarryoverFilters,
|
||||||
buildInventoryRootFrameFromAddressDebug,
|
buildInventoryRootFrameFromAddressDebug,
|
||||||
|
hydrateInventoryRootFrameState,
|
||||||
readAddressDebugFilters,
|
readAddressDebugFilters,
|
||||||
readAddressDebugItem,
|
readAddressDebugItem,
|
||||||
readAddressDebugTemporalScope,
|
readAddressDebugTemporalScope,
|
||||||
|
|
@ -732,46 +734,19 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate
|
hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate
|
||||||
? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage)
|
? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage)
|
||||||
: null;
|
: null;
|
||||||
let inventoryRootFrame =
|
const inventoryRootFrameCandidate =
|
||||||
deps.findRecentInventoryRootFrame(items) ??
|
deps.findRecentInventoryRootFrame(items) ??
|
||||||
continuitySnapshot.inventoryRootFrame ??
|
continuitySnapshot.inventoryRootFrame ??
|
||||||
buildInventoryRootFrameFromAddressDebug(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString);
|
buildInventoryRootFrameFromAddressDebug(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString);
|
||||||
if (inventoryRootFrame && navigationOrganization && !deps.toNonEmptyString(inventoryRootFrame.filters?.organization)) {
|
let { inventoryRootFrame, currentFrameKind } = hydrateInventoryRootFrameState(
|
||||||
inventoryRootFrame = {
|
inventoryRootFrameCandidate,
|
||||||
...inventoryRootFrame,
|
sourceIntent,
|
||||||
filters: {
|
navigationOrganization,
|
||||||
...(inventoryRootFrame.filters ?? {}),
|
navigationDateScope,
|
||||||
organization: navigationOrganization
|
deps.toNonEmptyString,
|
||||||
}
|
deps.isInventoryDrilldownFrameIntent,
|
||||||
};
|
deps.isInventoryRootFrameIntent
|
||||||
}
|
);
|
||||||
if (inventoryRootFrame && navigationDateScope) {
|
|
||||||
inventoryRootFrame = {
|
|
||||||
...inventoryRootFrame,
|
|
||||||
filters: {
|
|
||||||
...(inventoryRootFrame.filters ?? {}),
|
|
||||||
as_of_date:
|
|
||||||
deps.toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ??
|
|
||||||
deps.toNonEmptyString(navigationDateScope.as_of_date) ??
|
|
||||||
undefined,
|
|
||||||
period_from:
|
|
||||||
deps.toNonEmptyString(inventoryRootFrame.filters?.period_from) ??
|
|
||||||
deps.toNonEmptyString(navigationDateScope.period_from) ??
|
|
||||||
undefined,
|
|
||||||
period_to:
|
|
||||||
deps.toNonEmptyString(inventoryRootFrame.filters?.period_to) ??
|
|
||||||
deps.toNonEmptyString(navigationDateScope.period_to) ??
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let currentFrameKind = inventoryRootFrame
|
|
||||||
? deps.isInventoryDrilldownFrameIntent(sourceIntent)
|
|
||||||
? "inventory_drilldown"
|
|
||||||
: deps.isInventoryRootFrameIntent(sourceIntent)
|
|
||||||
? "inventory_root"
|
|
||||||
: "generic"
|
|
||||||
: null;
|
|
||||||
let resolvedCounterpartyFromDisplay = false;
|
let resolvedCounterpartyFromDisplay = false;
|
||||||
let displayedEntityTargetIntent = null;
|
let displayedEntityTargetIntent = null;
|
||||||
let previousFilters = resolveAddressDebugCarryoverFilters(previousAddressDebug, deps.toNonEmptyString);
|
let previousFilters = resolveAddressDebugCarryoverFilters(previousAddressDebug, deps.toNonEmptyString);
|
||||||
|
|
@ -919,7 +894,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
previousIntent = null;
|
previousIntent = null;
|
||||||
previousAnchorType = null;
|
previousAnchorType = null;
|
||||||
previousAnchor = null;
|
previousAnchor = null;
|
||||||
previousFilters = deps.buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
previousFilters = buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame, deps.toNonEmptyString);
|
||||||
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
||||||
followupSelectionMode = "carry_root_context";
|
followupSelectionMode = "carry_root_context";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
buildRootScopedCarryoverFilters,
|
||||||
|
hydrateInventoryRootFrameState,
|
||||||
readAddressDebugTemporalScope,
|
readAddressDebugTemporalScope,
|
||||||
resolveAddressDebugCarryoverFilters,
|
resolveAddressDebugCarryoverFilters,
|
||||||
resolveAddressDebugContextFacts,
|
resolveAddressDebugContextFacts,
|
||||||
|
|
@ -133,4 +135,68 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||||
period_to: "2021-03-31"
|
period_to: "2021-03-31"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hydrates inventory root-frame state from navigation scope and preserves derived current frame kind", () => {
|
||||||
|
const state = hydrateInventoryRootFrameState(
|
||||||
|
{
|
||||||
|
intent: "inventory_on_hand_as_of_date",
|
||||||
|
filters: {
|
||||||
|
warehouse: "Main Warehouse"
|
||||||
|
},
|
||||||
|
anchorType: "organization",
|
||||||
|
anchorValue: "Org Alt",
|
||||||
|
currentFrameKind: null
|
||||||
|
},
|
||||||
|
"inventory_purchase_documents_for_item",
|
||||||
|
"Org Alt",
|
||||||
|
{
|
||||||
|
as_of_date: "2021-03-31",
|
||||||
|
period_from: "2021-03-01",
|
||||||
|
period_to: "2021-03-31"
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(intent) => String(intent ?? "") === "inventory_purchase_documents_for_item",
|
||||||
|
(intent) => String(intent ?? "") === "inventory_on_hand_as_of_date"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.currentFrameKind).toBe("inventory_drilldown");
|
||||||
|
expect(state.inventoryRootFrame).toMatchObject({
|
||||||
|
currentFrameKind: "inventory_drilldown",
|
||||||
|
filters: {
|
||||||
|
organization: "Org Alt",
|
||||||
|
warehouse: "Main Warehouse",
|
||||||
|
as_of_date: "2021-03-31",
|
||||||
|
period_from: "2021-03-01",
|
||||||
|
period_to: "2021-03-31"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds root-scoped carryover filters with previous date precedence over inventory root frame", () => {
|
||||||
|
const filters = buildRootScopedCarryoverFilters(
|
||||||
|
{
|
||||||
|
organization: "Org Alt",
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
period_from: "2020-03-01",
|
||||||
|
period_to: "2020-03-31"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
organization: "Org Alt",
|
||||||
|
warehouse: "Main Warehouse",
|
||||||
|
as_of_date: "2021-03-31",
|
||||||
|
period_from: "2021-03-01",
|
||||||
|
period_to: "2021-03-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filters).toEqual({
|
||||||
|
organization: "Org Alt",
|
||||||
|
warehouse: "Main Warehouse",
|
||||||
|
as_of_date: "2020-03-31",
|
||||||
|
period_from: "2020-03-01",
|
||||||
|
period_to: "2020-03-31"
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy";
|
import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy";
|
||||||
import { buildRootScopedCarryoverFiltersForTests } from "../src/services/assistantService";
|
import { buildRootScopedCarryoverFilters } from "../src/services/assistantContinuityPolicy";
|
||||||
|
|
||||||
function toNonEmptyString(value: unknown): string | null {
|
function toNonEmptyString(value: unknown): string | null {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
|
|
@ -661,7 +661,7 @@ describe("assistantTransitionPolicy", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers the freshest previous date scope over a stale inventory root frame during same-date pivot", () => {
|
it("prefers the freshest previous date scope over a stale inventory root frame during same-date pivot", () => {
|
||||||
const filters = buildRootScopedCarryoverFiltersForTests(
|
const filters = buildRootScopedCarryoverFilters(
|
||||||
{
|
{
|
||||||
organization: 'ООО "Альтернатива Плюс"',
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
as_of_date: "2020-03-31",
|
as_of_date: "2020-03-31",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue