Лана: blocking_tools._web_search_sync, web_lookup.parse_result, test_web_lookup.py.
Отчёт пишется по ходу работы, а не в конце.
Мои файлы: blocking_tools.py, web_lookup.py, test_web_lookup.py. Всё остальное только читал.
Git не касался.
Скрипт _measure_urls.py (разовый, удалён после замера): вытащил все ссылки из
stomat_archive.db (117 847 реплик) и stomat_wiki.db (12 784 факта), прогнал каждую через обе
формы, которые сегодня склеивает подпроцесс, и сравнил вынутое регуляркой с исходным.
архив: строк со ссылкой 592, ссылок извлечено 628
вики: ссылок извлечено 5
уникальных ссылок всего: 556
из них со скобкой в самой ссылке: 1
форма tavily ("текст (url)"): ссылка не восстановлена в 1 из 556
форма ddgs ("текст\n(Source: url)"): ссылка не восстановлена в 1 из 556
было https://ru.m.wikipedia.org/wiki/%D0%9E%D0%BA%D1%81%D0%B8%D0%B4_%D1%86%D0%B8%D1%80%D0%BA%D0%BE%D0%BD%D0%B8%D1%8F(IV)
стало https://ru.m.wikipedia.org/wiki/%D0%9E%D0%BA%D1%81%D0%B8%D0%B4_%D1%86%D0%B8%D1%80%D0%BA%D0%BE%D0%BD%D0%B8%D1%8F(IV
Ссылка на «Оксид циркония(IV)» — не выдуманный пример, это реальная ссылка, которую врач
присылал в чат, и материал (диоксид циркония) стоматологический. Закрывающая скобка теряется,
потому что класс символов в регулярке [^\s\)\]]+ останавливается на первой ). Врач получает
ссылку, которая не открывается: утверждение выглядит подтверждённым, а проверить его нельзя.
Доля 1/556 = 0.18% выглядит мелко, но в живом архиве всего 2 ссылки на википедию и скобка
у одной из двух: ru.wikipedia именно так разводит стоматологические омонимы —
Пломба_(стоматология), Мост_(стоматология), Коронка_(стоматология). То есть класс попадает
ровно по тому запросу, ради которого энциклопедия и держится в списке источников (уровень 4 —
«определение термина»).
Скрипт _measure_shape.py (разовый, удалён): провайдеры подменены фальшивыми модулями, сети нет.
tavily отдал: ['Биодентин закрывает перфорацию. (https://ru.wikipedia.org/wiki/Пломба_(стоматология))']
тип элемента: str
разбор вернул url: 'https://ru.wikipedia.org/wiki/Пломба_(стоматология' <- битая
ddgs отдал: ['МТА даёт лучший прогноз (мета-анализ).\n(Source: https://ru.m.wikipedia.org/wiki/Оксид_циркония(IV))']
тип элемента: str
разбор вернул url: 'https://ru.m.wikipedia.org/wiki/Оксид_циркония(IV' <- битая
Подтверждено: наружу уходят СТРОКИ, в двух разных формах, и ссылка в обеих ломается.
Если в тексте выдержки есть свой маркер со ссылкой (страницы такое содержат: блок «источник», цитата, ссылка партнёра), маркерная регулярка срабатывает на ссылку ИЗ ТЕКСТА, а не на ту, которую вернул провайдер. Дальше по этому хосту считается уровень доверия и работает отсев рекламы:
строковая форма, выдержка из pubmed с чужим маркером внутри:
принято: [] | отсеяно как реклама: ['implant-msk.ru: домен рекламный (implant)']
структурная форма той же пары:
принято: ['pubmed.ncbi.nlm.nih.gov'] | отсеяно как реклама: []
Систематический обзор из PubMed выбрасывается целиком, и врач получает NOTHING_USABLE —
«в открытых источниках нашлась только реклама клиник». Это ложь в лицо: обзор был на руках.
Честно: частоту этого класса на живых данных я НЕ измерил — в 117 847 репликах архива готовый
маркер (Источник: url) внутри текста встречается 0 раз, а страницы провайдеров мне
недоступны (сеть в тесте запрещена и правильно). То есть класс показан, но не взвешен.
rg -g '!.git' -g '!*.log' по всему дереву:
perform_search:
search_engine.py:33 определение
search_engine_safe.py:10 определение
web_lookup.py:5,26 упоминания в комментарии
search_engine (как модуль):
ни одного import ни в одном .py; только комментарии (summarizer.py:9 — «импортировался,
но не вызывался ни разу», удалён) и отчёты _fix_/_recon_
динамический импорт (importlib / __import__): 17 совпадений, ни одного с search_engine
не-py файлы (start.bat и прочее): 0 совпадений
Вердикт прямой: search_engine.py мёртв целиком — его perform_search не вызывает никто,
_run_ddg_sync тоже, модуль никем не импортируется, включая динамический импорт и start.bat.
Более того, он опасен на импорте: from ddgs import DDGS и TavilyClient(api_key=...) на уровне
модуля, то есть импорт лезет в конфиг за ключом. search_engine_safe.py мёртв так же —
его perform_search не вызывается ниоткуда, он лишь обёртка над живым
blocking_tools.web_search_async. Не удаляю: решение лида.
Перепроверено мной заново (rg по ВСЕМУ дереву, не только по *.py, с ограничением области):
perform_search встречается только двумя определениями плюс упоминания в комментариях и
отчётах; строка search_engine не встречается ни в одном import. Вердикт прошлого прогона
подтверждаю.
search_engine_safe.py:22 склеивает результаты строкой: "\n\n".join(results). Список словарей
такой join не принимает. Замер (_measure_shape2.py, сети нет, провайдер подменён):
НОВАЯ форма (dict): вернул врачу: 'Ошибка поиска.' ссылка в ответе: False
в журнале: search subprocess wrapper failed: sequence item 0: expected str instance, dict found
СТАРАЯ форма (str): вернул врачу: 'Биодентин закрывает перфорацию. (https://pubmed...)' True
TypeError гасится широким except Exception на строке 23, и врач получает Ошибка поиска. на
КАЖДЫЙ успешный поиск — при найденных источниках на руках. Сегодня это не стреляет: обёртку никто
не вызывает. Но именно её web_lookup называет обёрткой поиска, то есть она первый кандидат на
подключение, и в момент подключения поиск будет отвечать «ошибка» с непустой выдачей.
Файл не мой — правку не делал, см. «Для лида».
Ссылку из ПОЛЯ мы теперь не трогаем — но текст выдержки чистился только от той ссылки, которую из него же и вынули. Все ОСТАЛЬНЫЕ ссылки оставались внутри выдержки и уезжали в промпт.
Замер на живых данных (_probe_text_urls.py, боевые базы через mode=ro):
stomat_archive.db: строк со ссылкой 591, из них ссылка в СЕРЕДИНЕ текста 110 (18.6%)
выдержка из pubmed с адресом клиники в середине текста:
до правки: рекламная ссылка ВНУТРИ промпта: True в подписи для врача: False
после: рекламная ссылка ВНУТРИ промпта: False в подписи для врача: False
строковая форма до правки: True, после: False (дефект был на ОБОИХ путях)
Последствие для врача: адрес клиники не проходил ни ранжирование по уровню, ни отсев рекламы, и в подписи его нет — но в выдержке он есть, а промпт велит опираться на выдержки. Модель цитирует его как часть систематического обзора. Врач получает ссылку на рекламу, выданную за источник уровня 1.
Правка: _strip_urls в web_lookup.py, применяется на ОБОИХ путях (структурном и строковом) —
расхождение путей и есть корень всей ланы. Выдержка, от которой после снятия ссылок не осталось
прозы, отбрасывается: источник с номером и пустым текстом даёт модели место, на которое можно
сослаться [1] под утверждением, которого в источнике нет.
18.6% — это доля по сообщениям ВРАЧЕЙ в архиве, а не по страницам провайдеров. Страницы провайдеров мне недоступны (сеть в тесте запрещена, и правильно), так что это ближайший доступный замер того, что ссылка внутри прозы на этом домене — рядовой случай, а не край.
Бэкапы _bak_shape_bt.py.bak / _bak_shape_wl.py.bak, после каждой диверсии восстановление и
сверка md5. Итоговые хеши совпадают с точкой до саботажа.
| # | Что сломано | Упало проверок | Поймано |
|---|---|---|---|
| 1 | _web_search_sync (tavily) снова клеит строку f"{content} ({url})" |
7 | да |
| 2 | parse_result: структурный путь не возвращает url из поля, а ищет в тексте |
8 | да |
| 3 | _trim_unbalanced_parens срезает ) безусловно |
5 | да |
| 4 | _as_search_entry не разбирает старые строки (parsed = None) |
4 | да |
| 5 | web_search_async не отсеивает пустышки |
2 | да |
| 6 | _as_search_entry молча глотает порченую нагрузку (снята запись в журнал) |
0 | НЕТ |
| 7 | тот же фильтр с and вместо or — находка без ссылки выбрасывается |
2 | да |
| 8 | _strip_urls превращён в no-op |
3 | да |
Диверсия 2 воспроизвела ровно тот отказ, ради которого лана существует: обзор с pubmed уехал в
ads как implant-msk.ru: домен рекламный (implant), и врач получил
«в открытых источниках нашлась только реклама клиник» — при обзоре на руках.
Снятие _throttled(...) из ветки «ни строка, ни структура» не уронило НИ ОДНОЙ из 94 проверок:
PASSED: 94 FAILED: 0, код возврата 0. Ни один тест не подавал в поиск None/int, то есть
ветка порченой нагрузки не исполнялась вовсе. Последствие: элемент выбрасывается молча — «поиск
нашёл 3, показал 1» без строки в журнале, и разобраться потом нечем.
Закрыто пятью проверками поведения (журнал ловится LogTrap по образцу test_report_targets.py):
порченая нагрузка не доезжает до врача, живая находка рядом не теряется, в журнале есть запись,
в записи назван ТИП, уровень не ниже WARNING. Повторный прогон той же диверсии — 3 проверки
падают.
Первый прогон диверсии 1 не просто провалил проверки — он ОБОРВАЛ тест: entry["url"] у строки
даёт TypeError, и оставшиеся ~25 проверок не запустились, а сводки PASSED/FAILED в выводе не
было вовсе. То есть поломка формы прятала за собой любую другую поломку разбора. Добавлен помощник
_entry(seq, index): не структура -> пустой словарь. После этого та же диверсия даёт честные
PASSED: 87 FAILED: 7 и все проверки доезжают.
Заодно две проверки вида «в тексте нет http» переписаны на «текст НЕПУСТОЙ и в нём нет http»: на пустом тексте первая форма проходила сама собой, то есть сломанная форма прошла бы как исправная.
test_web_lookup PASSED: 106 FAILED: 0 (было 66 до ланы, +40)
test_rag_quality PASSED: 53 FAILED: 0
test_fix_subprocess PASSED: 59 FAILED: 0
test_import_safety PASSED: 260 FAILED: 0
test_budget_nesting PASSED: 29 FAILED: 0
test_observability PASSED: 133 FAILED: 0
test_fix_cascade PASSED: 102 FAILED: 0
test_gemini_pacing PASSED: 18 FAILED: 0
test_validator_policy PASSED: 35 FAILED: 0
test_search_case PASSED: 33 FAILED: 0
test_isolation даёт PASSED: 106 FAILED: 2, и это НЕ моя лана:
test_media_loop_guard.py изолирует базу -- пишет в боевую stomat_bot.db. Файл чужой, не трогал.
- Живого поиска не было ни одного. Все провайдеры подменены фальшивыми модулями, сеть не
трогалась. Что tavily и ddgs сегодня реально отдают в полях
content/urlиbody/href, проверено по коду вызова, а не ответом сервиса. - Дочерний процесс целиком не запускался. Проверен
_web_search_syncпрямым вызовом иweb_search_asyncс подменённым_run_json_tool; настоящийpython blocking_tools.py web-searchсо stdin/stdout не прогонялся, то есть сериализация проверенаjson.dumps/loadsв том же процессе, а не через реальный канал. - Частота «чужой маркер внутри текста» на страницах провайдеров не измерена. В 117 847
репликах архива готовый
(Источник: url)внутри текста встречается 0 раз; 18.6% — это ссылка в середине прозы, близкий, но не тот же самый класс. Класс показан, но не взвешен. - Отбрасывание выдержки без прозы — решение с потерей. Источник с рабочей ссылкой, у которого весь текст был одной ссылкой, теперь не показывается врачу вовсе. Считаю это правильным (пустой источник с номером хуже), но это выбор, а не доказанный факт.
_strip_urlsвырезает и полезные ссылки. Если провайдер вернул выдержку, где ссылка на DOI стоит внутри прозы, врач её в тексте не увидит — только основной источник в подписи. Замера «как часто внутри выдержки лежит ссылка ЛУЧШЕ основной» нет.- Ничего из этого не вызывается ботом. Вся лана остаётся мёртвым кодом до подключения:
web_search_asyncне вызывает никто, кроме тестов и мёртвой обёртки.
search_engine_safe.py:22— ломается о новую форму."\n\n".join(results)на списке словарей даётTypeError, широкийexceptна :23 превращает его вОшибка поиска.для врача при НЕПУСТОЙ выдаче. Файл не мой. Правка на две строки:return "\n\n".join(f"{e['text']} ({e['url']})" if e.get("url") else e["text"] for e in results)— либо, что честнее, выкинуть обёртку и зватьweb_lookup.prepare, ради которого лана и делалась.search_engine.pyиsearch_engine_safe.pyмёртвы оба (замер выше).search_engine.pyвдобавок дёргает конфиг за ключом на уровне модуля. Удаление — решение лида, не делал.test_isolationловит чужое нарушение:test_media_loop_guard.pyпишет в боевуюstomat_bot.db. Это лана media, не моя.clip_at_sentenceвweb_lookup.pyдублирует_clip_at_sentenceвassistant.py— унаследованный комментарий обещает свести их в одно, когда ядро освободится. Не мой файл.