ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов Stage 3.7 ХВОСТЫ фикс маршрутов по домену задолжностей
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user