Архитектура: стабилизировать organization authority после late company switch и закрыть phase16 multi-company replay

This commit is contained in:
2026-04-19 15:31:50 +03:00
parent 6e6f94b08c
commit af15e21bf6
25 changed files with 1063 additions and 36 deletions
@@ -1374,6 +1374,14 @@ function hasSelectedObjectScopeSignal(text: string): boolean {
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
}
function isReferentialSameDateWarehousePhrase(candidate: string): boolean {
const normalized = cleanupAnchorValue(candidate)
.toLowerCase()
.replace(/ё/g, "е")
.trim();
return /^(?:по)\s+(?:этой|той)(?:\s+же)?\s+дат(?:е|у|ой)$/iu.test(normalized);
}
function extractInventoryWarehouseAnchor(text: string): string | undefined {
const patterns = [
/(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu,
@@ -1396,6 +1404,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
candidate.includes("->") ||
candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
isReferentialSameDateWarehousePhrase(candidate) ||
isLowQualityWarehouseAnchorValue(candidate) ||
normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) ||
@@ -1832,6 +1832,35 @@ function sameOrganizationEntityReference(left: string | null | undefined, right:
return organizationsLikelySameEntity(left, right);
}
function isReferentialOrganizationScopeValue(value: string | null | undefined): boolean {
const normalized = normalizeOrganizationScopeSearchText(value ?? "");
if (!normalized) {
return false;
}
return /^(?:эта|этой|эту|этой же|эта же|данная|данной|данную)\s+(?:компания|организация|фирма|контора)$/iu.test(normalized) ||
/^(?:по|у|для)\s+(?:этой|этой же|данной)\s+(?:компании|организации|фирме|конторе)$/iu.test(normalized) ||
/^(?:по|у|для)\s+ней$/iu.test(normalized);
}
function hasReferentialOrganizationScopeSignal(userMessage: string | null | undefined): boolean {
const normalized = normalizeOrganizationScopeSearchText(userMessage ?? "");
if (!normalized) {
return false;
}
return /(?:^| )(?:по|у|для)\s+(?:этой|этой же|данной)\s+(?:компании|организации|фирме|конторе)(?: |$)/iu.test(normalized) ||
/(?:^| )(?:по|у|для)\s+ней(?: |$)/iu.test(normalized) ||
/(?:^| )(?:эта|этой|эту|эта же|данная|данной)\s+(?:компания|организация|фирма|контора)(?: |$)/iu.test(normalized);
}
function isQuestionFragmentPartyAnchor(value: string | null | undefined): boolean {
const normalized = normalizeSearchText(String(value ?? ""));
if (!normalized) {
return false;
}
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
}
function applyPreExecutionOrganizationScopeGrounding(input: {
userMessage: string;
filters: AddressFilterSet;
@@ -1847,6 +1876,7 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
activeOrganization
]);
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
if (
!input.filters.organization &&
@@ -1879,6 +1909,41 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
}
}
if (
activeOrganization &&
((typeof input.filters.organization === "string" &&
isReferentialOrganizationScopeValue(input.filters.organization)) ||
referentialOrganizationScopeDetected) &&
!sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization)
) {
input.filters.organization = activeOrganization;
if (!input.warnings.includes("organization_grounded_from_referential_scope")) {
input.warnings.push("organization_grounded_from_referential_scope");
}
if (!input.baseReasons.includes("organization_grounded_from_referential_scope")) {
input.baseReasons.push("organization_grounded_from_referential_scope");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = activeOrganization;
}
}
if (
activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
typeof input.filters.counterparty === "string" &&
(isLikelyLowQualityPartyAnchor(input.filters.counterparty) ||
isQuestionFragmentPartyAnchor(input.filters.counterparty))
) {
delete input.filters.counterparty;
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
}
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
}
}
if (!input.filters.organization && !activeOrganization && !resolvedOrganizationFromMessage && candidateOrganizations.length === 1) {
input.filters.organization = candidateOrganizations[0];
if (!input.warnings.includes("organization_auto_selected_from_single_scope_candidate")) {
@@ -98,6 +98,10 @@ function hasSameDateHint(text: string): boolean {
);
}
function hasSameDatePrepositionHint(text: string): boolean {
return /(?:по\s+(?:этой|той)\s+же\s+дат(?:е|у|ой))/iu.test(String(text ?? ""));
}
function hasSamePeriodHint(text: string): boolean {
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|на\s+этот\s+период|за\s+тот\s+период|на\s+тот\s+период|этот\s+период|тот\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(
String(text ?? "")
@@ -814,7 +818,7 @@ function mergeFollowupFilters(
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage) || hasSameDatePrepositionHint(userMessage);
const samePeriodRequested = hasSamePeriodHint(userMessage);
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
@@ -1000,6 +1004,27 @@ function mergeFollowupFilters(
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
if (
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
previousPeriodFrom &&
merged.period_from !== previousPeriodFrom
) {
merged.period_from = previousPeriodFrom;
reasons.push("period_from_from_followup_context");
}
if (
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
previousPeriodTo &&
merged.period_to !== previousPeriodTo
) {
merged.period_to = previousPeriodTo;
reasons.push("period_to_from_followup_context");
}
const currentWarehouse = toNonEmptyString(merged.warehouse);
if (currentWarehouse && hasSameDatePrepositionHint(currentWarehouse)) {
delete merged.warehouse;
reasons.push("warehouse_cleared_from_same_date_followup_noise");
}
}
if (
samePeriodRequested &&
@@ -1019,15 +1019,15 @@ export function resolveAssistantOrganizationAuthority(
assistantSignals.lastAssistantActiveOrganization ??
continuityActiveOrganization ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
const organizationClarificationCandidates = Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations([
...input.lastOrganizationClarificationDebug.organization_candidates,
...knownOrganizations,
selectedOrganization,
activeOrganization,
continuityActiveOrganization
])
: [];
const organizationClarificationCandidates = mergeKnownOrganizations([
...(Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates)
? input.lastOrganizationClarificationDebug.organization_candidates
: []),
...knownOrganizations,
selectedOrganization,
activeOrganization,
continuityActiveOrganization
]);
const organizationClarificationSelectionFromScope = selectedOrganization ?? activeOrganization;
return {
@@ -65,6 +65,9 @@ export function resolveSessionOrganizationScopeContextRuntime<ItemType = unknown
const continuityKnownOrganizations = Array.isArray(continuityAuthority.knownOrganizations)
? continuityAuthority.knownOrganizations
: [];
const continuitySelectedOrganization = input.normalizeOrganizationScopeValue(
continuityAuthority.selectedOrganization
);
const continuityActiveOrganization = input.normalizeOrganizationScopeValue(
continuityAuthority.activeOrganization
);
@@ -87,8 +90,9 @@ export function resolveSessionOrganizationScopeContextRuntime<ItemType = unknown
);
const activeOrganization =
selectedOrganization ??
navigationActiveOrganization ??
continuitySelectedOrganization ??
continuityActiveOrganization ??
navigationActiveOrganization ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
return {
@@ -113,7 +117,8 @@ export function mergeFollowupContextWithOrganizationScopeRuntime(
previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...(previousFiltersRaw as Record<string, unknown>) }
: {};
if (!input.toNonEmptyString(previousFilters.organization)) {
const previousOrganization = input.toNonEmptyString(previousFilters.organization);
if (!previousOrganization || previousOrganization !== normalizedOrganization) {
previousFilters.organization = normalizedOrganization;
}
base.previous_filters = previousFilters;
@@ -122,7 +127,8 @@ export function mergeFollowupContextWithOrganizationScopeRuntime(
rootFiltersRaw && typeof rootFiltersRaw === "object"
? { ...(rootFiltersRaw as Record<string, unknown>) }
: {};
if (!input.toNonEmptyString(rootFilters.organization)) {
const rootOrganization = input.toNonEmptyString(rootFilters.organization);
if (!rootOrganization || rootOrganization !== normalizedOrganization) {
rootFilters.organization = normalizedOrganization;
}
if (Object.keys(rootFilters).length > 0) {
@@ -218,6 +218,7 @@ export function createAssistantRoutePolicy(deps) {
hasLooseAllTimeAddressLookupSignal,
hasDeepAnalysisPreferenceSignal,
hasDirectDeepAnalysisSignal,
shouldEmitOrganizationSelectionReply,
compactWhitespace,
hasDeepSessionContinuationSignal,
resolveLivingAssistantModeDecision,
@@ -526,6 +527,13 @@ export function createAssistantRoutePolicy(deps) {
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const organizationScopeSwitchDetected = Boolean(organizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
(shouldEmitOrganizationSelectionReply(rawUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(repairedRawUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(effectiveAddressUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(repairedEffectiveAddressUserMessage, organizationClarificationSelection)));
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const baseToolGate = typeof resolveAddressToolGateDecisionOverride === "function"
? resolveAddressToolGateDecisionOverride(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage)
@@ -732,6 +740,37 @@ export function createAssistantRoutePolicy(deps) {
}
};
}
if (organizationScopeSwitchDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "organization_scope_switch_detected",
livingMode: "chat",
livingReason: "organization_scope_switch_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext || continuitySnapshot.hasGroundedAddressContext),
organization_scope_switch_detected: true,
organization_scope_selection: organizationClarificationSelection,
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "organization_scope_switch_detected",
living_mode: "chat",
living_reason: "organization_scope_switch_detected"
}
}
};
}
const supportedExactInvestigativeAddressBypass = Boolean(llmContractMode === "deep_analysis" &&
semanticApplyCanonicalRecommended &&
strictDeepInvestigationBypassAllowed &&
@@ -142,6 +142,72 @@ const INVENTORY_SELECTED_OBJECT_TESTS = [
"full_anchor_not_degraded_by_canonical_rewrite"
] as const;
const SHARED_ROOT_EXACT_TESTS = [
"root_context_survives_domain_pivot_without_object_leak",
"limited_mode_remains_truthful"
] as const;
function rootExactCapability(input: {
capability_id: string;
domainId: string;
intent_ids: AddressIntent[];
transitions: AssistantTransitionClassId[];
requiredAnchors: string[];
optionalAnchors?: string[];
resultShape: string;
answerObjectShape: string;
scenarioFamilies?: string[];
}): AssistantCapabilityContract {
return {
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
capability_id: input.capability_id,
domain_id: input.domainId,
runtime_lane: "address_exact",
intent_ids: input.intent_ids,
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
supported_transition_classes: input.transitions,
frame_compatibility: {
root_frame: "optional",
selected_object_frame: "optional",
meta_frame: "forbidden"
},
required_anchors: input.requiredAnchors,
optional_anchors: input.optionalAnchors ?? ["organization", "date_scope", "account", "counterparty", "contract"],
anchor_source_priority: ["explicit_user_anchor", "root_frame", "semantic_hint"],
anchor_admissibility_rules: [
"confirmed_root_scope_beats_semantic_hint",
"no_low_quality_counterparty_rewrite",
"no_conversational_noise_as_entity"
],
organization_scope_behavior: "reuse_or_clarify",
date_scope_behavior: "reuse",
temporal_ceiling_policy: "must_not_expand_without_reason_code",
root_context_compatibility: "required",
requires_focus_object: false,
accepted_focus_object_kinds: [],
focus_object_override_policy: "not_applicable",
bundle_reuse_policy: "none",
resolver_owner: "addressIntentResolver",
recipe_owner: "addressRecipeCatalog",
execution_adapter: "AddressQueryService",
result_shape: input.resultShape,
answer_object_shape: input.answerObjectShape,
minimum_evidence_policy: "route_specific_threshold",
coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"],
blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"],
clarification_triggers: ["ambiguous_organization_scope", "ambiguous_date_scope"],
clarification_questions: ["Уточните организацию, счёт или дату, чтобы не подставлять неподтверждённый контур."],
resume_policy: "resume_original_route_with_resolved_anchors",
empty_match_behavior: "truthful_empty_match",
route_expectation_failure_behavior: "blocked_route_expectation_failure",
execution_error_behavior: "blocked_execution_error",
required_unit_tests: [...SHARED_ROOT_EXACT_TESTS],
required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`),
required_scenario_families: input.scenarioFamilies ?? ["canonical", "colloquial", "followup_date_carryover"]
};
}
function inventoryExactCapability(input: {
capability_id: string;
intent_ids: AddressIntent[];
@@ -285,6 +351,71 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
})
] as const;
export const ROOT_EXACT_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
rootExactCapability({
capability_id: "confirmed_payables_as_of_date",
domainId: "counterparty_debt",
intent_ids: ["payables_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "counterparty_payables_snapshot",
answerObjectShape: "payables_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_receivables_as_of_date",
domainId: "counterparty_debt",
intent_ids: ["receivables_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "counterparty_receivables_snapshot",
answerObjectShape: "receivables_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_open_contracts_as_of_date",
domainId: "contracts",
intent_ids: ["open_contracts_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "open_contracts_snapshot",
answerObjectShape: "open_contracts_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_vat_payable_as_of_date",
domainId: "vat",
intent_ids: ["vat_payable_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "vat_payable_snapshot",
answerObjectShape: "vat_payable_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_vat_liability_for_tax_period",
domainId: "vat",
intent_ids: ["vat_liability_confirmed_for_tax_period"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "vat_tax_period_liability_snapshot",
answerObjectShape: "vat_tax_period_liability_snapshot",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover", "tax_period_followup"]
}),
rootExactCapability({
capability_id: "account_balance_exact",
domainId: "accounting_balance",
intent_ids: ["account_balance_snapshot", "documents_forming_balance"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: ["account"],
optionalAnchors: ["organization", "date_scope", "account"],
resultShape: "account_balance_snapshot_or_supporting_documents",
answerObjectShape: "account_balance_context",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover", "same_date_account_followup"]
})
] as const;
const ALL_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
...INVENTORY_CAPABILITY_CONTRACTS,
...ROOT_EXACT_CAPABILITY_CONTRACTS
] as const;
export function listAssistantTransitionContracts(): readonly AssistantTransitionContract[] {
return ASSISTANT_TRANSITION_CONTRACTS;
}
@@ -298,9 +429,9 @@ export function listInventoryCapabilityContracts(): readonly AssistantCapability
}
export function getAssistantCapabilityContract(capabilityId: string): AssistantCapabilityContract | null {
return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
return ALL_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
}
export function getAssistantCapabilityContractByIntent(intent: AddressIntent): AssistantCapabilityContract | null {
return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
return ALL_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
}
@@ -4049,6 +4049,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
hasLooseAllTimeAddressLookupSignal,
hasDeepAnalysisPreferenceSignal,
hasDirectDeepAnalysisSignal,
shouldEmitOrganizationSelectionReply: assistantLivingModePolicy.shouldEmitOrganizationSelectionReply,
compactWhitespace,
hasDeepSessionContinuationSignal,
resolveLivingAssistantModeDecision: assistantLivingModePolicy.resolveLivingAssistantModeDecision,