
Во время работы над очередной задачей я опечатался в пути к выгрузке. Удивительно, но конвейер отработал без ошибок. Он вернул пустой индекс, добавил предупреждение и спокойно передал результат дальше:
before: индекс total = 0
warnings: cf_export_root not found or not a directory
Он не упал, а создал правдоподобный пустой индекс. Агент после этого мог уверенно объяснять, почему в конфигурации нет нужной формы, хотя настоящая причина была проще: до конфигурации никто не дошёл. Оставлять так было нельзя.
После исправления тот же вход останавливает выполнение:
after: NotADirectoryError:
cf_export_root must be an existing directory
Именно с этой разницы начинается доверие к инструменту. Не с количества тестов и не с гладкости ответа модели, а со способности конвейера честно сообщить, что произошло с данными.
Коротко: что делать
-
Проверяйте вход до начала обхода. Неверный каталог не должен превращаться в пустой результат.
-
Различайте три уровня неполноты: объект не найден, данные не извлечены, связь не доказана.
-
Сохраняйте причину неполноты в машинном статусе, а артефакты делайте версионированными и переносимыми.

Работает не значит заслуживает доверия
До эпика инструмент уже умел обходить подготовленную выгрузку, строить индекс форм, читать дерево элементов и собирать компактный контекст. Был 821 тест, десять примеров и рабочие сценарии.
Это нормальное состояние исследовательского проекта. Полезный результат уже есть, а эксплуатационный контракт ещё складывается по частям.
Проблема начинается, когда такой результат передают автоматике. Человек может заметить подозрительно пустой индекс, странное предупреждение или путь от чужой машины. Скрипт увидит код возврата 0 и пойдёт дальше. Модель тем более не знает, что происходило до формирования промпта. Она отвечает на то, что получила.
Получились свои «пятьдесят оттенков зелёного»: отношения с качеством данных оказались куда сложнее, чем обещал цвет индикатора. Тесты проходят, индекс строится, команда завершается успешно, а доверять результату всё ещё рано. Общий результат эпика в другом: конвейер начал показывать, какой вход получил, что обработал, где потерял полноту и какой части результата доверять нельзя.
Первый уровень: проверяем вход
Проверка корня выгрузки кажется слишком простой для отдельной задачи. Но именно на таких местах и возникают дорогие ошибки.
Раньше несуществующий каталог давал индекс с total = 0 и предупреждением. Для вызывающего кода это был обычный результат. Пустая конфигурация, опечатка и недоступный каталог выглядели одинаково.
Теперь контракт жёсткий:
from pathlib import Path
root = Path("/путь/к/выгрузке")
if not root.is_dir():
raise NotADirectoryError(
f"cf_export_root must be an existing directory: {root}"
)
Важно различить три вида пустоты:
-
каталог существует, данные действительно пусты;
-
каталог существует, но часть данных не прочитана;
-
вход вообще не существует или не является каталогом.
Если вернуть во всех трёх случаях одинаковый пустой результат, доказательства причины исчезнут ещё до того, как данные увидит модель. Поэтому ошибка входа должна останавливать конвейер до создания правдоподобного пустого результата.
Есть и второй вопрос к входу: что именно выгружено. Файловая выгрузка конфигурации содержит читаемые XML (Extensible Markup Language - расширяемый язык разметки) файлы и подготовленные деревья элементов. Цельный контейнер конфигурации устроен иначе. Одинаковая команда поиска по этим двум представлениям отвечает на разные вопросы.
Второй уровень: проверяем обнаружение
Модульный тест обычно подаёт форму обработчику и проверяет результат. Такой тест отвечает на вопрос «правильно ли обработана найденная форма». Он ничего не говорит о формах, которые до обработчика не дошли. А что, удобно: нет формы - нет проблемы.
Поэтому полнота обнаружения требует отдельного сравнения:
files = list(export_root.rglob("*.elem.json"))
index = scan_forms(export_root)
assert index.total == len(files)
На трёх рабочих распакованных выгрузках эта проверка дала:
|
Выгрузка |
Form.bin |
*.elem.json |
Форм в индексе |
Потеря |
|---|---|---|---|---|
|
A |
0 |
2216 |
2216 |
0 |
|
B |
0 |
3738 |
3738 |
0 |
|
C |
0 |
76 |
76 |
0 |
Это важный нулевой результат. На этих входах нет бинарных макетов, а число записей в индексе точно совпадает с числом подготовленных файлов форм. Ни до эпика, ни после него потеря на обнаружении здесь не воспроизводится.
Откуда взялись 84,4 процента
Я решил подготовить точку расширения под будущую обработку бинарных форм и начал с исследования трёх конфигураций. В них суммарно нашлось 4782 файла Form.bin: 1354, 6 и 3422. На крупнейшей выборке старый механизм учитывал только 535 из 3422 источников. Остальные 2887, или 84,4 %, терялись ещё до попытки разобрать содержимое: 2822 из-за коллизий имён и 65 из-за другого расположения общих форм.
Исправление ввело устойчивый form_id и перестало схлопывать разные источники в одну запись. Но здесь надо понимать, что обнаружение источника и разбор его содержимого - разные способности.
v8unpack-agent научился корректно обнаруживать и различать бинарные источники. Он не получил встроенный промышленный преобразователь Form.bin в текстовый слой. Этот шаг задан контрактом FormUnpacker:
FormUnpacker = Callable[[FormBinSource, Path], FormArtifact]
Реализацию передаёт вызывающая сторона. На всех 14 проверенных образцах попытка передать распаковщику одиночный Form.bin вместо контейнера целиком завершилась ошибкой. Это оказался неподдерживаемый сценарий входа, а не доказанный дефект upstream-инструмента, поэтому отдельная задача для него не создавалась.
Проверка полноты должна отвечать на вопрос о вашей выгрузке. Чужой процент нельзя переносить между разными представлениями конфигурации.
Третий уровень: проверяем, что удалось понять
Форма может быть найдена и при этом остаться понятой лишь частично. У неё может не быть объекта-владельца. Данные объекта-владельца могут отсутствовать или не прочитаться. Ссылочный тип может присутствовать только в виде уникального идентификатора.
Раньше часть этих состояний схлопывалась в одинаковую пустоту. Для модели разницы не было:
{
"Properties": []
}
Но «у объекта нет реквизитов» и «реквизиты не удалось прочитать» означают противоположные вещи. В первом случае пустой список является фактом. Во втором он скрывает отказ.
На контрольной выгрузке 162 из 2216 форм имеют состояние no_owner_object. Это отдельный штатный класс форм без определённого объекта-владельца, а не ошибка чтения. Другие причины получили отдельные предупреждения вместо общей пустоты.
Недоказанный тип остаётся недоказанным
У ссылочного реквизита в служебных данных может быть только UUID (Universally Unique Identifier - универсальный уникальный идентификатор). Чтобы получить имя, его нужно сопоставить с индексом типов конфигурации или с подтверждённой таблицей платформенных типов.
Если сопоставление не удалось, конвейер сохраняет:
Ref#a1b2c3d4-0000-0000-0000-000000000000
Имя реквизита может подталкивать к очевидной догадке. Но угаданное имя выглядит в контексте так же, как доказанное. Поэтому остаток не достраивается по позиции, виду объекта или смыслу имени.
На полном пути обработки контрольной выгрузки получилось:
|
Показатель |
До второго уровня резолюции |
После |
|---|---|---|
|
Применимых ссылочных вхождений |
15 723 |
15 723 |
|
Резолвлено |
14 422 |
15 448 |
|
Доля |
91,73 % |
98,25 % |
|
Остаток |
1301 |
275 |
Из оставшихся 275 вхождений 117 относятся к семи UUID без доказанного имени, ещё 158 - к 28 идентификаторам с известным определением, но недоказанной связью с машинным именем типа. Исследование комбинации «вид объекта плюс позиция» не дало ни одного устойчивого имени на трёх конфигурациях. Поэтому остаток сохранён.
Почему null недостаточно
В комментариях к прошлой статье возник ещё один вопрос: что должна увидеть модель, если связь, вероятно, существует, но путь к ней не установлен. Обычный null этого не объясняет. Он может означать как «значения нет», так и «значение не удалось определить».
Для строгого контракта полезно различать хотя бы два состояния:
data_path: null
status: unresolved
reason: unknown_layout
Здесь объект или путь может существовать, но текущая раскладка не распознана. Другой случай:
data_path: null
status: unresolved
reason: not_found
Здесь соответствующий объект не найден в доступной выгрузке. Пока это только пример того, как контракт должен выглядеть: таких полей в пакете ещё нет.
Важно не смешивать две неопределённости. data_path относится к резолюции пути данных, а Ref#uuid - к имени ссылочного типа. Задача #289 касается второй. Для первой отдельный отрицательный статус рядом с полем также остаётся открытой границей.
Предупреждение в общем массиве хуже локального признака: при сокращении контекста массив может не дойти до модели, а null останется. Отрицательная информация должна находиться рядом с тем значением, к которому относится. Иначе разные причины снова превратятся в одну пустоту.
Ответ читателю: что стало с 556 ссылками
Также в комментариях к предыдущей статье читатель спросил, что скрывается за 556 неразрешёнными ссылками и видно ли самой модели, что связь не доказана. Я обещал вернуться с результатами.
Методика изменилась, поэтому напрямую сравнивать 556 и 275 нельзя даже в процентах: отличаются область обхода, знаменатель и единица подсчёта.
|
Измерение |
Что считалось |
Всего ссылок |
Остаток |
|---|---|---|---|
|
Раннее |
прямой вызов декодера |
5226 |
556 |
|
Текущее |
полный путь от индекса до контекста |
15 723 |
275 |
Чтобы ответить именно про прежние 556, старую ссылочную выборку повторно прогнали текущим резолвером. Из них 397 получили подтверждённое платформенное имя, 159 остались Ref#uuid. Обещанное исследование десяти самых частых UUID выполнено. В повторном замере всей старой выборки имена получили 13 из 49 уникальных идентификаторов, а 36 остались без доказанного имени. Всего на той же ссылочной выборке разрешено 5067 из 5226.
Ссылочная выборка воспроизвелась точно, хотя общий счётчик реквизитов между версиями инструмента отличался.
Вторая часть обещания пока не закрыта. В итоговом LLM-фрагменте нет отдельного поля type_resolved: false. Единственный сигнал для модели - сама строка Ref#uuid. Существующий флаг resolved относится к разрешению пути данных, а не ссылочного типа.
На оставшиеся части заведены задачи #288 и #289. Продолжение следует, но срок на этот раз обещать не буду.
Четвёртый уровень: проверяем статус прогона
Человек прочитает предупреждение. Регламентный скрипт обычно смотрит только на код возврата.
У запуска появились обязательный post-run report и явные классы завершения:
|
Код |
Результат |
|---|---|
|
0 |
обработка завершена полностью |
|
2 |
ошибка аргументов или корня выгрузки |
|
3 |
есть частично обработанные или ошибочные объекты |
|
4 |
управляемая фатальная ошибка конвейера |
|
5 |
не удалось записать отчёт |
|
6 |
фатальная ошибка вмест |
Ключевой код здесь 3. Частичный результат больше не выглядит успехом для вызывающей автоматизации.
Предупреждения получили шесть стабильных машинных кодов. На трёх проверенных выгрузках сообщение об отсутствующем модуле изменилось так:
before:
skipped (no .obj.bsl): Catalog/Demo/CatalogForm/ФормаСписка
after:
skipped (no .obj.bsl): Catalog/Demo/CatalogForm/ФормаСписка
[code=FORM_MODULE_MISSING]
Граница текущего отчёта тоже важна. Он показывает полноту извлечения: какие объекты обработаны полностью, частично или с ошибкой. Он пока не показывает полноту разрешения ссылочных типов. Объект с
Если частичный результат возвращает код 0 и не имеет машинной причины, для автоматизации он неотличим от полного.
Пятый уровень: проверяем переносимость
До эпика индекс содержал абсолютные пути. На трёх рабочих выгрузках число вхождений корня было таким:
|
Выгрузка |
До |
После |
|---|---|---|
|
A |
6648 |
0 |
|
B |
11 214 |
0 |
|
C |
228 |
0 |
Абсолютный путь делает артефакт зависимым от машины и раскрывает структуру рабочего каталога. Теперь пути относительные и записываются в едином формате.
Но эта проверка доказывает только переносимость штатного сериализованного индекса. Она не доказывает, что абсолютный путь никогда не попадёт в текст исключения, traceback или служебное представление объекта. Для такого утверждения нужен отдельный тест: намеренно выбросить исключение с синтетическим абсолютным путём и проверить весь аварийный канал перед выдачей результата.
Поэтому корректная граница звучит так: абсолютные пути удалены из проверенных штатных артефактов. Универсальная санитизация исключений и traceback этим замером не подтверждена.
Одновременно у схемы появилась версия:
{
"schema_version": 2,
"total": 2216,
"forms": []
}
Без версии старый файл можно принять за актуальный и получить тихо неверный результат. С версией несовместимость становится явным состоянием.
Вывод примеров стал детерминированным. Два запуска на неизменных данных дают побайтово одинаковый результат, поэтому изменения можно проверять обычным diff.
На тех же трёх выгрузках проверился второй уровень разрешения платформенного UUID:
before: None
after: 'cfg:CatalogRef'
Это уже не исследовательская метрика на чужом корпусе, а воспроизведённое изменение на трёх собственных выгрузках. Артефакту можно доверять в автоматизации, только если его можно перенести, сравнить и однозначно прочитать другой версией инструмента.
Чем подтверждается результат
Я прогнал одну и ту же проверку на последнем коммите до эпика и на итоговом коммите после него:
|
Проверка |
До |
После |
|---|---|---|
|
Pytest |
821 |
1194 |
|
Ruff |
211 ошибок |
0 |
|
Mypy |
15 ошибок в 5 из 30 файлов |
0 в 41 файле |
|
Побочные модули при импорте |
4 |
2 |
|
Примеры |
10 |
15 |
|
Версия индекса |
нет |
2 |
|
Runtime-зависимости |
pytest и v8unpack из git |
v8unpack>=1.2.13 |
Итоговый коммит прошёл шесть обязательных проверок CI (Continuous Integration - автоматическая сборка и проверка) на Ubuntu и Windows с Python 3.10 и 3.12. Пакет опубликован в PyPI как версия 0.1.0.
Эти числа не доказывают полноту конкретной конфигурации. Они доказывают воспроизводимость контракта инструмента. Полнота входа всё равно проверяется отдельно на каждой выгрузке.
Да, чуть не забыл: это ещё и первый релиз пакета. Теперь его можно попробовать обычной установкой из PyPI. Отдельное спасибо авторам upstream-пакета v8unpack за свежий релиз, без которого этот выпуск пришлось бы отложить. Это тот релиз, который в своём проекте иногда ждёшь сильнее, чем GPT-6 и GTA VI.
Тесты подтверждают заявленный контракт, но не заменяют измерение данных, которых не было в тестовой выборке.
Что эпик не сделал
После длинного списка проверок легко написать слишком сильный вывод. Поэтому перечислю границы отдельно.
-
Пакет не получил встроенный промышленный распаковщик Form.bin. Он обнаруживает источник и передаёт его внешнему FormUnpacker.
-
Исправленная идентичность источников не означает, что их бинарное содержимое успешно разобрано.
-
Результат на одной конфигурации не гарантирует такое же покрытие на другой.
-
Оставшиеся ссылочные типы не получили выдуманных имён.
-
Post-run report пока не показывает долю разрешённых типов. Это задача #288.
-
LLM-фрагмент пока не имеет отдельного признака недоказанного типа. Это задача #289.
-
Относительность путей подтверждена для штатных артефактов, но универсальная санитизация исключений и traceback отдельно не доказана.
-
Полный технический прогон не доказывает правильность бизнес-вывода модели.
Иначе говоря, эпик не сделал агента умнее. Он сделал происхождение и границы переданных агенту данных заметнее.
Что бы я сделал иначе
-
Сначала описал бы состояния complete, partial и failed. Я добавлял обработку раньше, чем договорился о смысле результата, поэтому тихие отказы пришлось разбирать задним числом. Сейчас статус является частью контракта.
-
Не менял бы методику метрик без воспроизводимого моста к старому замеру. Из-за разных знаменателей 556 и 275 легко принять за прямое улучшение. Теперь сравнение повторяется на одной ссылочной выборке.
-
Проверял бы несколько видов выгрузки до общего вывода. Отсутствие Form.bin в распакованном дереве я однажды принял за отсутствие бинарных форм в исходной конфигурации. Сейчас сначала фиксируется тип входа.
Чек-лист
-
Определите, что перед вами: файловая выгрузка, подготовленное дерево или цельный контейнер.
-
Перед обходом проверьте, что корень существует и является каталогом.
-
Посчитайте входные файлы форм по каждому поддерживаемому виду.
-
Сравните число файлов с числом записей в индексе.
-
Запустите конвейер на несуществующем пути и убедитесь, что он падает явно.
-
Разделите в модели данных «пусто», «не применимо» и «не удалось прочитать».
-
Посчитайте остаток Ref#uuid командой python examples/unresolved_refs_report.py <корень>.
-
Убедитесь, что неизвестный тип не достраивается по имени реквизита или позиции.
-
Проверьте ненулевой код завершения частичного прогона.
-
Проверьте наличие стабильных машинных кодов у предупреждений.
-
Найдите абсолютный корень выгрузки в сохранённом индексе. Вхождений должно быть ноль.
-
Намеренно выбросьте исключение с синтетическим абсолютным путём и проверьте отчёт, текст ошибки и traceback.
-
Проверьте версию схемы и поведение на старом артефакте.
-
Запустите обработку дважды и сравните результаты через diff.
Что дальше
На ближайшие технические доработки уже заведены задачи #288 и #289. Следующий большой шаг - обогащение уже подготовленного контекста через RAG (Retrieval-Augmented Generation - метод дополнения LLM контекстом из индекса). Хочу связать поиск релевантных форм и сбор данных для LLM в единый воспроизводимый путь, не пряча границы распаковки и разрешения связей.
Ссылки
-
v8unpack-agent - публичный Python-пакет подготовки форм 1С к анализу агентом, MIT (лицензия Массачусетского технологического института).
-
saby-integration/v8unpack - open-source распаковщик контейнеров 1С.
-
Roadmap технической зрелости и качества данных - закрытый эпик из пятидесяти задач, на котором основана статья.
-
Исследование бинарных источников форм - закрытое исследование масштаба и стадий потери.
-
Диагностика остатка в отчёте о прогоне - открытая задача.
-
Явный признак недоказанного типа в LLM-фрагменте - открытая задача.
-
Как дать LLM контекст формы 1С без выдуманных связей - предыдущая статья и источник вопроса о неразрешённых ссылках.
-
Обычные формы 1С в агентном конвейере - начало практической части серии.
-
Реестр форм 1С для агента - устройство индекса форм.
-
Управляемые формы 1С после распаковки - различие сценариев выгрузки и единый конвейер.
Вступайте в нашу телеграмм-группу Инфостарт