ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Архитектурный фундамент: capability-guard, navigation state и трассируемый route-контракт

This commit is contained in:
2026-04-12 14:40:48 +03:00
parent fbd156e58e
commit c10260dedf
22 changed files with 1842 additions and 23 deletions
@@ -1,8 +1,12 @@
import {
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1,
FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1
} from "../config";
import type {
AddressCapabilityLayer,
AddressCapabilityRouteMode,
AddressShadowRouteStatus,
AddressAsOfDateBasis,
AddressEvidenceStrength,
AddressExecutionResult,
@@ -25,6 +29,11 @@ import { executeAddressMcpQuery } from "./addressMcpClient";
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
import { composeFactualReply, inferReplyType, type ComposeReplySemantics } from "./address_runtime/composeStage";
import {
isCapabilityRouteBlocked,
resolveAddressCapabilityRouteDecision,
resolveShadowRouteIntent
} from "./addressCapabilityPolicy";
interface NormalizedAddressRow {
period: string | null;
@@ -40,6 +49,20 @@ interface AddressTryHandleOptions {
analysisDateHint?: string | null;
}
interface AddressCapabilityAudit {
capabilityId: string;
layer: AddressCapabilityLayer;
routeMode: AddressCapabilityRouteMode;
enabled: boolean;
reason: string;
}
interface AddressShadowRouteAudit {
intent: AddressIntent | null;
selectedRecipe: string | null;
status: AddressShadowRouteStatus;
}
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;
@@ -904,6 +927,45 @@ function withConfirmedBalanceFallbackReason(
return [...reasons, "confirmed_balance_unavailable_fallback_to_heuristic_candidates"];
}
function buildCapabilityAudit(intent: AddressIntent): AddressCapabilityAudit {
const decision = resolveAddressCapabilityRouteDecision(intent);
return {
capabilityId: decision.capability_id,
layer: decision.capability_layer,
routeMode: decision.capability_route_mode,
enabled: decision.capability_route_enabled,
reason: decision.capability_route_reason
};
}
function buildShadowRouteAudit(input: {
intent: AddressIntent;
requestedResultMode: AddressResultMode | undefined;
filters: AddressFilterSet;
}): AddressShadowRouteAudit {
const shadowIntent = resolveShadowRouteIntent(input.intent, input.requestedResultMode);
if (!shadowIntent) {
return {
intent: null,
selectedRecipe: null,
status: "skipped"
};
}
const shadowRecipeSelection = selectAddressRecipe(shadowIntent, input.filters);
if (!shadowRecipeSelection.selected_recipe) {
return {
intent: shadowIntent,
selectedRecipe: null,
status: "unavailable"
};
}
return {
intent: shadowIntent,
selectedRecipe: shadowRecipeSelection.selected_recipe.recipe_id,
status: "planned"
};
}
function enforceStrictAccountScopeForIntent(
plan: AddressRecipeExecutionPlan,
intent: AddressIntent
@@ -1701,6 +1763,8 @@ function buildLimitedExecutionResult(input: {
reasonText: string;
nextStep?: string;
category: AddressLimitedReasonCategory;
capabilityAudit?: AddressCapabilityAudit;
shadowRouteAudit?: AddressShadowRouteAudit;
}): AddressExecutionResult {
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
const resultSemantics = deriveAddressResultSemantics({
@@ -1772,6 +1836,14 @@ function buildLimitedExecutionResult(input: {
runtime_readiness: runtimeReadinessForLimitedCategory(input.category),
limited_reason_category: input.category,
response_type: "LIMITED_WITH_REASON",
capability_id: input.capabilityAudit?.capabilityId ?? null,
capability_layer: input.capabilityAudit?.layer ?? null,
capability_route_mode: input.capabilityAudit?.routeMode ?? null,
capability_route_enabled: input.capabilityAudit?.enabled ?? true,
capability_route_reason: input.capabilityAudit?.reason ?? null,
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
...resultSemantics,
limitations: input.limitations,
reasons
@@ -1826,6 +1898,36 @@ export class AddressQueryService {
baseReasons.push("as_of_date_derived_for_confirmed_payables");
}
}
const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({
intent: intent.intent,
requestedResultMode,
filters: executionFilters
});
if (isCapabilityRouteBlocked(capabilityDecision)) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: executionFilters,
missingRequiredFilters: [],
selectedRecipe: null,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "маршрут capability временно отключен feature-флагом",
nextStep: "включите capability route или используйте соседний поддерживаемый сценарий",
limitations: ["capability_route_disabled_by_flag"],
reasons: [
...baseReasons,
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 ? "capability_route_guard_blocked" : "capability_route_guard_skipped"
],
capabilityAudit,
shadowRouteAudit
});
}
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
@@ -1889,7 +1991,9 @@ export class AddressQueryService {
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
reasons: baseReasons,
capabilityAudit,
shadowRouteAudit
});
}
@@ -1909,7 +2013,9 @@ export class AddressQueryService {
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
reasons: [...baseReasons, ...recipeSelection.selection_reason],
capabilityAudit,
shadowRouteAudit
});
}
@@ -1929,7 +2035,9 @@ export class AddressQueryService {
reasonText: "не хватает обязательных фильтров",
nextStep: `уточните: ${recipeSelection.missing_required_filters.join(", ")}`,
limitations: ["missing_required_filters"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
reasons: [...baseReasons, ...recipeSelection.selection_reason],
capabilityAudit,
shadowRouteAudit
});
}
@@ -1949,7 +2057,9 @@ export class AddressQueryService {
reasonText: "live address lane выключен feature-флагом",
nextStep: "включите FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1",
limitations: ["address_live_lane_disabled"],
reasons: baseReasons
reasons: baseReasons,
capabilityAudit,
shadowRouteAudit
});
}
@@ -2087,7 +2197,9 @@ export class AddressQueryService {
reasonText: "live MCP вызов завершился ошибкой",
nextStep: mcp.error,
limitations: ["mcp_call_failed"],
reasons: [...baseReasons, mcp.error]
reasons: [...baseReasons, mcp.error],
capabilityAudit,
shadowRouteAudit
});
}
@@ -2862,7 +2974,9 @@ export class AddressQueryService {
reasonText,
nextStep,
limitations,
reasons: baseReasons
reasons: baseReasons,
capabilityAudit,
shadowRouteAudit
});
}
@@ -2904,7 +3018,9 @@ export class AddressQueryService {
reasonText: "exact payables mode: confirmed balance was not proven for the requested as-of slice",
nextStep: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"]
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"],
capabilityAudit,
shadowRouteAudit
});
}
return {
@@ -2949,6 +3065,14 @@ export class AddressQueryService {
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
limited_reason_category: null,
response_type: factual.responseType,
capability_id: capabilityAudit.capabilityId,
capability_layer: capabilityAudit.layer,
capability_route_mode: capabilityAudit.routeMode,
capability_route_enabled: capabilityAudit.enabled,
capability_route_reason: capabilityAudit.reason,
shadow_route_intent: shadowRouteAudit.intent,
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
shadow_route_status: shadowRouteAudit.status,
...factualResultSemantics,
limitations: filters.warnings,
reasons: withConfirmedBalanceFallbackReason(