ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Архитектура маршрутов v2: baseline ожиданий intentrecipe/result_mode с runtime-аудитом

This commit is contained in:
2026-04-12 14:54:52 +03:00
parent bd1fbbdb67
commit 278eb4abeb
14 changed files with 624 additions and 6 deletions
@@ -1,5 +1,7 @@
import {
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1,
FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1,
FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1,
FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1
} from "../config";
@@ -34,6 +36,7 @@ import {
resolveAddressCapabilityRouteDecision,
resolveShadowRouteIntent
} from "./addressCapabilityPolicy";
import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations";
interface NormalizedAddressRow {
period: string | null;
@@ -63,6 +66,14 @@ interface AddressShadowRouteAudit {
status: AddressShadowRouteStatus;
}
interface AddressRouteExpectationAuditState {
status: AddressRouteExpectationAudit["status"];
reason: string;
expectedSelectedRecipes: string[];
expectedRequestedResultModes: AddressResultMode[];
expectedResultModes: AddressResultMode[];
}
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1" as const;
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
@@ -966,6 +977,36 @@ function buildShadowRouteAudit(input: {
};
}
function buildRouteExpectationAudit(input: {
intent: AddressIntent;
selectedRecipe: string | null;
requestedResultMode?: AddressResultMode;
resultMode?: AddressResultMode;
}): AddressRouteExpectationAuditState {
if (!FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1) {
return {
status: "not_found",
reason: "route_expectation_audit_disabled",
expectedSelectedRecipes: [],
expectedRequestedResultModes: [],
expectedResultModes: []
};
}
const audit = evaluateAddressRouteExpectation({
intent: input.intent,
selectedRecipe: input.selectedRecipe,
requestedResultMode: input.requestedResultMode,
resultMode: input.resultMode
});
return {
status: audit.status,
reason: audit.reason,
expectedSelectedRecipes: audit.expected_selected_recipes,
expectedRequestedResultModes: audit.expected_requested_result_modes,
expectedResultModes: audit.expected_result_modes
};
}
function enforceStrictAccountScopeForIntent(
plan: AddressRecipeExecutionPlan,
intent: AddressIntent
@@ -1765,6 +1806,7 @@ function buildLimitedExecutionResult(input: {
category: AddressLimitedReasonCategory;
capabilityAudit?: AddressCapabilityAudit;
shadowRouteAudit?: AddressShadowRouteAudit;
routeExpectationAudit?: AddressRouteExpectationAuditState;
}): AddressExecutionResult {
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
const resultSemantics = deriveAddressResultSemantics({
@@ -1786,6 +1828,14 @@ function buildLimitedExecutionResult(input: {
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response")
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"]
: reasonsWithConfirmedFallback;
const routeExpectationAudit =
input.routeExpectationAudit ??
buildRouteExpectationAudit({
intent: input.intent.intent,
selectedRecipe: input.selectedRecipe,
requestedResultMode: requestedResultMode,
resultMode: resultSemantics.result_mode
});
return {
handled: true,
reply_text: composeLimitedReply({
@@ -1844,6 +1894,11 @@ function buildLimitedExecutionResult(input: {
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
route_expectation_status: routeExpectationAudit.status,
route_expectation_reason: routeExpectationAudit.reason,
route_expectation_expected_selected_recipes: routeExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: routeExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: routeExpectationAudit.expectedResultModes,
...resultSemantics,
limitations: input.limitations,
reasons
@@ -2991,6 +3046,45 @@ export class AddressQueryService {
}),
factual.semantics
);
const finalRouteExpectationAudit = buildRouteExpectationAudit({
intent: intent.intent,
selectedRecipe: effectiveRecipeId,
requestedResultMode,
resultMode: factualResultSemantics.result_mode
});
if (finalRouteExpectationAudit.status === "mismatch" && FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: [],
selectedRecipe: effectiveRecipeId,
accountScopeMode: plan.account_scope_mode,
accountScopeFallbackApplied,
accountScopeAudit,
anchor,
matchFailureStage,
matchFailureReason,
mcpCallStatus: stageStatus,
rowsFetched: mcp.fetched_rows,
rawRowsReceived: mcp.raw_rows.length,
rowsAfterAccountScope: normalizedRows.length,
rowsAfterRecipeFilter: filterByAnchors.length,
rowsMaterialized: normalizedRows.length,
rowsMatched: filteredRows.length,
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
materializationDropReason: rowDiagnostics.materializationDropReason,
category: "recipe_visibility_gap",
reasonText: "маршрут не прошел baseline route expectation contract",
nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout",
limitations: ["route_expectation_mismatch_guard_blocked"],
reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`],
capabilityAudit,
shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit
});
}
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
return buildLimitedExecutionResult({
mode,
@@ -3020,9 +3114,14 @@ export class AddressQueryService {
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"],
capabilityAudit,
shadowRouteAudit
shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit
});
}
const reasonsWithRouteExpectation =
finalRouteExpectationAudit.status === "mismatch"
? [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`]
: baseReasons;
return {
handled: true,
reply_text: factual.text,
@@ -3073,10 +3172,15 @@ export class AddressQueryService {
shadow_route_intent: shadowRouteAudit.intent,
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
shadow_route_status: shadowRouteAudit.status,
route_expectation_status: finalRouteExpectationAudit.status,
route_expectation_reason: finalRouteExpectationAudit.reason,
route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes,
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
...factualResultSemantics,
limitations: filters.warnings,
reasons: withConfirmedBalanceFallbackReason(
baseReasons,
reasonsWithRouteExpectation,
requestedResultMode,
factual.semantics,
factualResultSemantics.result_mode