From 6467f77661e855a9f73f571e13db4964bff9c199 Mon Sep 17 00:00:00 2001 From: andy24kr <117143136+andy24kr@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:52:00 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=83=D1=81=D0=B5=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20=D0=B2=20?= =?UTF-8?q?explain=5Fsnippet=20=D0=B4=D0=BE=20=D0=BB=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit explain_snippet заявляет лимит MAX_SNIPPET_CHARS (4000), но собранный из сниппета signal_text уходил в search() без усечения и отбивался проверкой MAX_QUERY_CHARS (500). В результате инструмент падал с "query is too long: max 500 characters" на любом реальном сниппете длиннее ~450 символов — то есть почти на любой процедуре 1С. В page() эта ситуация уже обработана явной проверкой длины перед вызовом search() (строка 533), в explain_snippet аналогичной защиты не было. Добавлена truncate_for_query(): усекает текст до MAX_QUERY_CHARS по границе слова. На релевантность не влияет — signal_text отсортирован по значимости, первыми идут выделенные сигналы. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/v8std_mcp_index.py | 14 +++++++++++++- tests/test_v8std_mcp_index.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/v8std_mcp_index.py b/scripts/v8std_mcp_index.py index a668834..18c9d24 100644 --- a/scripts/v8std_mcp_index.py +++ b/scripts/v8std_mcp_index.py @@ -109,6 +109,17 @@ def require_text(value: Any, field_name: str, max_chars: int) -> str: return value +def truncate_for_query(text: str, max_chars: int = MAX_QUERY_CHARS) -> str: + """Обрезает построенный поисковый текст до лимита query, не разрывая слово.""" + if len(text) <= max_chars: + return text + cut = text[:max_chars] + boundary = cut.rfind(" ") + if boundary > max_chars // 2: + cut = cut[:boundary] + return cut.strip() + + def require_string_list(values: Any, field_name: str, item_max_chars: int) -> list[str] | None: if values is None: return None @@ -592,7 +603,8 @@ def explain_snippet( *signal_targets, ] ) - search_result = self.search(signal_text or snippet, types=None, mode="hybrid", limit=limit) + search_text = truncate_for_query(signal_text or snippet) + search_result = self.search(search_text, types=None, mode="hybrid", limit=limit) diagnostics = [] standards = [] diff --git a/tests/test_v8std_mcp_index.py b/tests/test_v8std_mcp_index.py index 06820d5..9bbc5b5 100644 --- a/tests/test_v8std_mcp_index.py +++ b/tests/test_v8std_mcp_index.py @@ -241,6 +241,25 @@ def test_related_and_batch_preserve_multiple_clauses_of_one_standard(self): expected, ) + def test_explain_snippet_accepts_long_snippet(self): + # Регресс: длинный сниппет падал с "query is too long: max 500 characters", + # хотя explain_snippet заявляет лимит MAX_SNIPPET_CHARS (4000). + snippet = ( + 'Процедура ПриЗаписи(Источник, Отказ) Экспорт\n' + '\tПопытка\n' + '\t\tОбработатьИсточник(Источник);\n' + '\tИсключение\n' + '\t\tЗаписьЖурналаРегистрации("Пример", УровеньЖурналаРегистрации.Ошибка, , ' + 'Источник.Ссылка, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));\n' + '\tКонецПопытки;\n' + 'КонецПроцедуры\n' + ) * 3 + self.assertGreater(len(snippet), 500) + self.assertLess(len(snippet), 4000) + result = self.index.explain_snippet(snippet) + self.assertIn("diagnostics", result) + self.assertIn("signals", result) + def test_explain_snippet_and_batch_diagnostics(self): snippet = self.index.explain_snippet( 'Запрос = Новый Запрос("ВЫБРАТЬ РАЗРЕШЕННЫЕ ...")'