ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ фикс маршрутов по домену задолжностей

This commit is contained in:
2026-04-11 23:34:59 +03:00
parent 160ed18fe5
commit d65969d2ff
16 changed files with 2248 additions and 237 deletions
@@ -715,6 +715,103 @@ function applyIntentSpecificFilter(intent: AddressIntent, rows: NormalizedAddres
return rows;
}
function parseIsoDateUtcTimestamp(value: string | null | undefined): number | null {
const source = String(value ?? "").trim();
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
if (month < 1 || month > 12 || day < 1 || day > 31) {
return null;
}
return Date.UTC(year, month - 1, day);
}
function isCounterpartyRiskIntent(intent: AddressIntent): boolean {
return (
intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" ||
intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract"
);
}
function resolveFutureGuardReferenceDate(analysisDate: string | null, filters: AddressFilterSet): string | null {
if (analysisDate) {
return analysisDate;
}
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
if (asOfDate) {
return asOfDate;
}
const periodTo = normalizeAnalysisDateHint(filters.period_to);
if (periodTo) {
return periodTo;
}
return null;
}
function isMissingSubcontoFieldError(errorText: string | null | undefined): boolean {
const normalized = String(errorText ?? "")
.toLowerCase()
.replace(/\s+/g, " ");
if (!normalized) {
return false;
}
return (
normalized.includes("поле не найдено") &&
(normalized.includes("субконтодт1") ||
normalized.includes("subcontodt1") ||
normalized.includes("subconto_dt1"))
);
}
function applyFutureDatedRowsGuard(
rows: NormalizedAddressRow[],
intent: AddressIntent,
referenceDate: string | null
): { rows: NormalizedAddressRow[]; droppedCount: number } {
if (!isCounterpartyRiskIntent(intent) || rows.length === 0) {
return {
rows,
droppedCount: 0
};
}
const referenceTs = (() => {
const explicitTs = parseIsoDateUtcTimestamp(referenceDate);
if (explicitTs !== null) {
return explicitTs;
}
const now = new Date();
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
})();
const guardTailMs = 31 * 24 * 60 * 60 * 1000;
const latestAllowedTs = referenceTs + guardTailMs;
const keptRows: NormalizedAddressRow[] = [];
let droppedCount = 0;
for (const row of rows) {
const rowTs = parseIsoDateUtcTimestamp(row.period);
if (rowTs !== null && rowTs > latestAllowedTs) {
droppedCount += 1;
continue;
}
keptRows.push(row);
}
return {
rows: keptRows,
droppedCount
};
}
function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
return (
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
@@ -745,6 +842,8 @@ function isAnchorRecoveryIntent(intent: AddressIntent): boolean {
intent === "list_contracts_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_contract" ||
intent === "list_payables_counterparties" ||
intent === "list_receivables_counterparties" ||
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts"
);
@@ -1483,10 +1582,20 @@ export class AddressQueryService {
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined
});
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, filters.extracted_filters);
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
const recipeSelection = selectAddressRecipe(intent.intent, filters.extracted_filters);
const debtLifecycleReceivablesScenario =
intent.intent === "list_receivables_counterparties" &&
Array.isArray(intent.reasons) &&
intent.reasons.includes("receivables_debt_lifecycle_signal_detected");
const recipeIntent = debtLifecycleReceivablesScenario ? "open_items_by_counterparty_or_contract" : intent.intent;
const recipeSelection = selectAddressRecipe(recipeIntent, filters.extracted_filters);
if (debtLifecycleReceivablesScenario && recipeIntent !== intent.intent) {
baseReasons.push("recipe_override_to_open_items_for_receivables_debt_lifecycle");
}
if (intent.intent === "unknown") {
return buildLimitedExecutionResult({
@@ -1508,30 +1617,6 @@ export class AddressQueryService {
});
}
if (
intent.intent === "open_items_by_counterparty_or_contract" &&
!filters.extracted_filters.counterparty &&
!filters.extracted_filters.contract
) {
return buildLimitedExecutionResult({
mode,
shape,
intent,
filters: filters.extracted_filters,
missingRequiredFilters: ["counterparty_or_contract"],
selectedRecipe: null,
anchor,
mcpCallStatus: "skipped",
rowsFetched: 0,
rowsMatched: 0,
category: "missing_anchor",
reasonText: "для open_items нужен якорь контрагента или договора",
nextStep: "укажите контрагента или номер/название договора",
limitations: ["open_items_requires_counterparty_or_contract_filter"],
reasons: baseReasons
});
}
if (recipeSelection.selected_recipe === null) {
return buildLimitedExecutionResult({
mode,
@@ -1631,11 +1716,40 @@ export class AddressQueryService {
}
}
const plan = buildAddressRecipePlan(recipeSelection.selected_recipe, filters.extracted_filters);
const mcp = await executeAddressMcpQuery({
let plan = buildAddressRecipePlan(recipeSelection.selected_recipe, filters.extracted_filters);
let mcp = await executeAddressMcpQuery({
query: plan.query,
limit: plan.limit
});
if (
mcp.error &&
recipeSelection.selected_recipe.recipe_id === "address_movements_receivables_v1" &&
isMissingSubcontoFieldError(mcp.error)
) {
const fallbackSelection = selectAddressRecipe("open_items_by_counterparty_or_contract", filters.extracted_filters);
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
const fallbackPlan = buildAddressRecipePlan(fallbackSelection.selected_recipe, filters.extracted_filters);
const fallbackMcp = await executeAddressMcpQuery({
query: fallbackPlan.query,
limit: fallbackPlan.limit
});
if (!fallbackMcp.error) {
plan = fallbackPlan;
mcp = fallbackMcp;
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
}
} else {
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
}
}
} else {
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
}
}
}
if (mcp.error) {
const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters);
@@ -1696,7 +1810,21 @@ export class AddressQueryService {
});
const anchorFilter = applyAddressFilters(normalizedRows, filtersForMatching);
const filterByAnchors = anchorFilter.rows;
const filteredRows = applyIntentSpecificFilter(intent.intent, filterByAnchors);
const filteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, filterByAnchors);
const filteredRowsFutureGuard = applyFutureDatedRowsGuard(
filteredRowsBeforeFutureGuard,
intent.intent,
futureGuardReferenceDate
);
const filteredRows = filteredRowsFutureGuard.rows;
if (filteredRowsFutureGuard.droppedCount > 0) {
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
filters.warnings.push("future_rows_excluded_from_response");
}
if (!baseReasons.includes("future_rows_excluded_from_response")) {
baseReasons.push("future_rows_excluded_from_response");
}
}
const rowDiagnostics = deriveRowStageDiagnostics(mcp.raw_rows, normalizedRows.length, normalizedRows.length);
const stageStatus = deriveMcpStageStatus({
rawRowsReceived: mcp.raw_rows.length,
@@ -1782,7 +1910,9 @@ export class AddressQueryService {
if (
filteredRows.length === 0 &&
isAnchorRecoveryIntent(intent.intent) &&
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")
(stageStatus === "materialized_but_not_anchor_matched" ||
stageStatus === "materialized_but_filtered_out_by_recipe" ||
stageStatus === "raw_rows_received_but_not_materialized")
) {
const currentLimit =
typeof filters.extracted_filters.limit === "number" && Number.isFinite(filters.extracted_filters.limit)
@@ -1827,7 +1957,21 @@ export class AddressQueryService {
});
const expandedAnchorFilter = applyAddressFilters(expandedNormalizedRows, expandedFiltersForMatching);
const expandedRowsByAnchor = expandedAnchorFilter.rows;
const expandedFilteredRows = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
const expandedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, expandedRowsByAnchor);
const expandedFutureGuard = applyFutureDatedRowsGuard(
expandedFilteredRowsBeforeFutureGuard,
intent.intent,
resolveFutureGuardReferenceDate(analysisDate, expandedLimitFilters)
);
const expandedFilteredRows = expandedFutureGuard.rows;
if (expandedFutureGuard.droppedCount > 0) {
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
filters.warnings.push("future_rows_excluded_from_response");
}
if (!baseReasons.includes("future_rows_excluded_from_response")) {
baseReasons.push("future_rows_excluded_from_response");
}
}
if (expandedFilteredRows.length > 0) {
const expandedRowDiagnostics = deriveRowStageDiagnostics(
expandedMcp.raw_rows,
@@ -1938,7 +2082,21 @@ export class AddressQueryService {
});
const broadenedAnchorFilter = applyAddressFilters(broadenedNormalizedRows, broadenedFiltersForMatching);
const broadenedRowsByAnchor = broadenedAnchorFilter.rows;
const broadenedFilteredRows = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor);
const broadenedFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, broadenedRowsByAnchor);
const broadenedFutureGuard = applyFutureDatedRowsGuard(
broadenedFilteredRowsBeforeFutureGuard,
intent.intent,
resolveFutureGuardReferenceDate(analysisDate, autoBroadenedFilters)
);
const broadenedFilteredRows = broadenedFutureGuard.rows;
if (broadenedFutureGuard.droppedCount > 0) {
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
filters.warnings.push("future_rows_excluded_from_response");
}
if (!baseReasons.includes("future_rows_excluded_from_response")) {
baseReasons.push("future_rows_excluded_from_response");
}
}
if (broadenedFilteredRows.length > 0) {
const broadenedRowDiagnostics = deriveRowStageDiagnostics(
broadenedMcp.raw_rows,
@@ -2059,7 +2217,21 @@ export class AddressQueryService {
});
const historicalAnchorFilter = applyAddressFilters(historicalNormalizedRows, historicalFiltersForMatching);
const historicalRowsByAnchor = historicalAnchorFilter.rows;
const historicalFilteredRows = applyIntentSpecificFilter(intent.intent, historicalRowsByAnchor);
const historicalFilteredRowsBeforeFutureGuard = applyIntentSpecificFilter(intent.intent, historicalRowsByAnchor);
const historicalFutureGuard = applyFutureDatedRowsGuard(
historicalFilteredRowsBeforeFutureGuard,
intent.intent,
resolveFutureGuardReferenceDate(analysisDate, historicalFilters)
);
const historicalFilteredRows = historicalFutureGuard.rows;
if (historicalFutureGuard.droppedCount > 0) {
if (!filters.warnings.includes("future_rows_excluded_from_response")) {
filters.warnings.push("future_rows_excluded_from_response");
}
if (!baseReasons.includes("future_rows_excluded_from_response")) {
baseReasons.push("future_rows_excluded_from_response");
}
}
if (historicalFilteredRows.length > 0) {
const historicalRowDiagnostics = deriveRowStageDiagnostics(
historicalMcp.raw_rows,