Архитектура: централизовать selected-item carryover в continuity policy и transition glue
This commit is contained in:
parent
4f56aa9b2a
commit
851289b9a6
|
|
@ -434,6 +434,11 @@ Still open after the accepted phase12 replay:
|
|||
- this matters because counterparty / contract / selected-entity follow-ups are one of the heaviest remaining sources of local carryover reconstruction, and moving them under the shared continuity layer reduces another chance that route retargeting and anchor state drift apart when new domains are added;
|
||||
- targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` regressions now protect both the helper layer and a real `displayed counterparty -> contracts` follow-up path;
|
||||
- the next proof after this pass should still come from a live replay, but the expected verdict should now only move if a real counterparty carryover path regresses rather than because the state mutation lived in an inline transition branch.
|
||||
- the next continuity-authority pass now removes another selected-item state owner from the transition hot path:
|
||||
- continuity now owns `applySelectedItemCarryover(...)`, so `previous_filters.item` plus `previous_anchor_*` for selected-object inventory follow-ups no longer mutate inline inside `assistantTransitionPolicy`;
|
||||
- item carryover precedence is now explicit in one helper: navigation focus item -> continuity active item -> explicit selected-object label from the current message;
|
||||
- this matters because selected-object follow-ups are one of the most fragile continuity seams in the product, and keeping their anchor mutation in the shared continuity layer reduces another chance that future inventory/domain expansion splits `focus_object` truth from follow-up carryover truth;
|
||||
- targeted continuity and transition regressions now protect both the helper layer and a real short selected-item follow-up path that must keep the `item` anchor without reopening company/date drift.
|
||||
- the next continuity-authority pass now centralizes temporal backfill precedence for follow-up filters:
|
||||
- transition no longer holds a service-local block of `shouldBackfillPreviousDateScopeFromNavigation + six field-level ifs` for `as_of_date / period_from / period_to`;
|
||||
- shared continuity now owns that merge via `applyTemporalCarryoverFilters(...)`, while `shouldUseNavigationTemporalCarryover(...)` keeps the intent-family boundary explicit in one place;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ exports.applyTemporalCarryoverFilters = applyTemporalCarryoverFilters;
|
|||
exports.applyOrganizationCarryoverFilters = applyOrganizationCarryoverFilters;
|
||||
exports.applyHistoricalPartyCarryoverFilters = applyHistoricalPartyCarryoverFilters;
|
||||
exports.applyReferencedEntityCarryover = applyReferencedEntityCarryover;
|
||||
exports.applySelectedItemCarryover = applySelectedItemCarryover;
|
||||
exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug;
|
||||
exports.isGroundedAddressDebug = isGroundedAddressDebug;
|
||||
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
|
||||
|
|
@ -352,6 +353,33 @@ function applyReferencedEntityCarryover(previousFilters, previousAnchorType, pre
|
|||
resolvedCounterpartyFromDisplay
|
||||
};
|
||||
}
|
||||
function applySelectedItemCarryover(previousFilters, previousAnchorType, previousAnchorValue, rootScopedPivot, shouldApplySelectedItemCarryover, navigationFocusItemLabel, continuityActiveItem, selectedObjectLabelFromUser, selectedObjectLabelFromAlternate, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
const nextFilters = previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
|
||||
if (rootScopedPivot || !shouldApplySelectedItemCarryover || toNonEmptyString(nextFilters.item)) {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue
|
||||
};
|
||||
}
|
||||
const selectedObjectLabel = toNonEmptyString(navigationFocusItemLabel) ??
|
||||
toNonEmptyString(continuityActiveItem) ??
|
||||
toNonEmptyString(selectedObjectLabelFromUser) ??
|
||||
toNonEmptyString(selectedObjectLabelFromAlternate);
|
||||
if (!selectedObjectLabel) {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue
|
||||
};
|
||||
}
|
||||
nextFilters.item = selectedObjectLabel;
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType: "item",
|
||||
previousAnchorValue: selectedObjectLabel
|
||||
};
|
||||
}
|
||||
function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||
if (!debug || typeof debug !== "object") {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -667,27 +667,19 @@ function createAssistantTransitionPolicy(deps) {
|
|||
followupSelectionMode,
|
||||
resolvedCounterpartyFromDisplay
|
||||
} = (0, assistantContinuityPolicy_1.applyReferencedEntityCarryover)(previousFilters, previousAnchorType, previousAnchor, followupSelectionMode, resolvedEntityFromFollowup, rootScopedPivot, deps.toNonEmptyString));
|
||||
if (!rootScopedPivot &&
|
||||
!deps.toNonEmptyString(previousFilters.item) &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate)) {
|
||||
const selectedObjectLabel = (navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null) ??
|
||||
continuitySnapshot.activeItem ??
|
||||
extractSelectedObjectLabel(userMessage) ??
|
||||
(deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null);
|
||||
if (selectedObjectLabel) {
|
||||
previousFilters.item = selectedObjectLabel;
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = selectedObjectLabel;
|
||||
}
|
||||
}
|
||||
({
|
||||
previousFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue: previousAnchor
|
||||
} = (0, assistantContinuityPolicy_1.applySelectedItemCarryover)(previousFilters, previousAnchorType, previousAnchor, rootScopedPivot, sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
sourceIntentHint === "inventory_profitability_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate, navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null, continuitySnapshot.activeItem, extractSelectedObjectLabel(userMessage), deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null, deps.toNonEmptyString));
|
||||
if (explicitOrganizationClarificationSelection && !previousAnchor) {
|
||||
previousAnchorType = "organization";
|
||||
previousAnchor = explicitOrganizationClarificationSelection;
|
||||
|
|
|
|||
|
|
@ -536,6 +536,53 @@ export function applyReferencedEntityCarryover(
|
|||
};
|
||||
}
|
||||
|
||||
export function applySelectedItemCarryover(
|
||||
previousFilters: Record<string, unknown> | null,
|
||||
previousAnchorType: string | null,
|
||||
previousAnchorValue: string | null,
|
||||
rootScopedPivot: boolean,
|
||||
shouldApplySelectedItemCarryover: boolean,
|
||||
navigationFocusItemLabel: unknown,
|
||||
continuityActiveItem: unknown,
|
||||
selectedObjectLabelFromUser: unknown,
|
||||
selectedObjectLabelFromAlternate: unknown,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
): {
|
||||
previousFilters: Record<string, unknown>;
|
||||
previousAnchorType: string | null;
|
||||
previousAnchorValue: string | null;
|
||||
} {
|
||||
const nextFilters =
|
||||
previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
|
||||
if (rootScopedPivot || !shouldApplySelectedItemCarryover || toNonEmptyString(nextFilters.item)) {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue
|
||||
};
|
||||
}
|
||||
|
||||
const selectedObjectLabel =
|
||||
toNonEmptyString(navigationFocusItemLabel) ??
|
||||
toNonEmptyString(continuityActiveItem) ??
|
||||
toNonEmptyString(selectedObjectLabelFromUser) ??
|
||||
toNonEmptyString(selectedObjectLabelFromAlternate);
|
||||
if (!selectedObjectLabel) {
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue
|
||||
};
|
||||
}
|
||||
|
||||
nextFilters.item = selectedObjectLabel;
|
||||
return {
|
||||
previousFilters: nextFilters,
|
||||
previousAnchorType: "item",
|
||||
previousAnchorValue: selectedObjectLabel
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInventoryRootFrameFromAddressDebug(
|
||||
debug: Record<string, unknown> | null,
|
||||
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
applyHistoricalPartyCarryoverFilters,
|
||||
applyOrganizationCarryoverFilters,
|
||||
applyReferencedEntityCarryover,
|
||||
applySelectedItemCarryover,
|
||||
applyTemporalCarryoverFilters,
|
||||
buildRootScopedCarryoverFilters,
|
||||
buildInventoryRootFrameFromAddressDebug,
|
||||
|
|
@ -874,10 +875,16 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
rootScopedPivot,
|
||||
deps.toNonEmptyString
|
||||
));
|
||||
if (
|
||||
!rootScopedPivot &&
|
||||
!deps.toNonEmptyString(previousFilters.item) &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
({
|
||||
previousFilters,
|
||||
previousAnchorType,
|
||||
previousAnchorValue: previousAnchor
|
||||
} = applySelectedItemCarryover(
|
||||
previousFilters,
|
||||
previousAnchorType,
|
||||
previousAnchor,
|
||||
rootScopedPivot,
|
||||
sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
|
||||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
|
||||
sourceIntentHint === "inventory_sale_trace_for_item" ||
|
||||
|
|
@ -885,19 +892,13 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
|
||||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate)
|
||||
) {
|
||||
const selectedObjectLabel =
|
||||
(navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null) ??
|
||||
continuitySnapshot.activeItem ??
|
||||
extractSelectedObjectLabel(userMessage) ??
|
||||
(deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null);
|
||||
if (selectedObjectLabel) {
|
||||
previousFilters.item = selectedObjectLabel;
|
||||
previousAnchorType = "item";
|
||||
previousAnchor = selectedObjectLabel;
|
||||
}
|
||||
}
|
||||
hasSelectedObjectInventorySignalAlternate,
|
||||
navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null,
|
||||
continuitySnapshot.activeItem,
|
||||
extractSelectedObjectLabel(userMessage),
|
||||
deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null,
|
||||
deps.toNonEmptyString
|
||||
));
|
||||
if (explicitOrganizationClarificationSelection && !previousAnchor) {
|
||||
previousAnchorType = "organization";
|
||||
previousAnchor = explicitOrganizationClarificationSelection;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
applyHistoricalPartyCarryoverFilters,
|
||||
applyOrganizationCarryoverFilters,
|
||||
applyReferencedEntityCarryover,
|
||||
applySelectedItemCarryover,
|
||||
applyTemporalCarryoverFilters,
|
||||
buildRootScopedCarryoverFilters,
|
||||
hydrateInventoryRootFrameState,
|
||||
|
|
@ -324,4 +325,29 @@ describe("assistantContinuityPolicy organization authority", () => {
|
|||
resolvedCounterpartyFromDisplay: true
|
||||
});
|
||||
});
|
||||
|
||||
it("applies selected-item carryover from navigation focus before continuity and explicit labels", () => {
|
||||
const carryover = applySelectedItemCarryover(
|
||||
{
|
||||
organization: "Org Alt"
|
||||
},
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
"Workstation Navigation",
|
||||
"Workstation Continuity",
|
||||
"Workstation User",
|
||||
"Workstation Alternate"
|
||||
);
|
||||
|
||||
expect(carryover).toEqual({
|
||||
previousFilters: {
|
||||
organization: "Org Alt",
|
||||
item: "Workstation Navigation"
|
||||
},
|
||||
previousAnchorType: "item",
|
||||
previousAnchorValue: "Workstation Navigation"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -297,6 +297,48 @@ describe("assistantTransitionPolicy", () => {
|
|||
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hydrates selected-item carryover through shared continuity helper for short object follow-up", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "Подтвержден складской срез по выбранной позиции.",
|
||||
debug: {
|
||||
detected_intent: "inventory_on_hand_as_of_date",
|
||||
extracted_filters: {
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2020-03-31"
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
hasShortInventoryObjectFollowupSignal: () => true,
|
||||
findRecentInventoryRootFrame: () => null,
|
||||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"по этой позиции покажи документы",
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
{
|
||||
session_context: {
|
||||
active_focus_object: {
|
||||
object_type: "item",
|
||||
label: "Workstation Focus"
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization: 'ООО "Альтернатива Плюс"',
|
||||
as_of_date: "2020-03-31",
|
||||
item: "Workstation Focus"
|
||||
});
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("item");
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Workstation Focus");
|
||||
});
|
||||
|
||||
it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
|
|
|
|||
Loading…
Reference in New Issue