Отказы: когда инструмент не делает того, о чём его попросили
У инструмента три места, где он отвечает «нет» или «не всё». Все три — осознанные, и каждое сообщает что-то полезное. Разбираем по очереди, с настоящим выводом.
Отказ первый: исходник не разобрался
Инструменту нужно найти в файле границы функций. Для этого он разбирает файл
настоящим разборщиком того языка: у Python — стандартным ast, у C и C++ —
libclang, у JavaScript — разборщиком из его же мира.
Если файл не разбирается, инструмент не сохраняет ничего.
Посмотрим. В песочнице уже лежит один нормальный файл, good.py. Пишем рядом
заведомо сломанный:
printf 'def broken(:\n return 1\n' | ouroboros write ./demo broken.py
[python] corrupted source in broken.py: invalid syntax (<unknown>, line 1)
Код возврата — 1. И, что важнее, файла в песочнице не появилось:
ls ./demo/draft/
good.py
ouroboros_runtime.py
broken.py нет. Не «есть, но пустой», не «есть, но необмазанный» — нет вовсе.
Почему так, а не иначе. Разборщик языка уже умеет отличать текст, который является программой, от текста, который ею не является. Раз он всё равно запускается на каждой записи, его вывод используется и как проверка. Файл, который не разобрался, не сохраняется — значит сломанного полуобмазанного состояния в песочнице не возникает никогда: либо файл разобран и обмазан, либо его там нет.
Отказ этот адресован вам, а не инструменту. Сообщение называет язык, файл, причину и строку — этого хватает, чтобы починить.
Отказ второй: вызов вошёл и не вернулся
Записей на вызов две. Значит вызов, который не завершился, оставляет одну — строку входа без парной строки выхода.
Такое бывает, когда программа зависла, получила сигнал или вышла жёстко, минуя обычный порядок. Проверим на программе, которая обрывает себя посередине:
import os
def never_returns(n):
os._exit(3)
def caller():
never_returns(7)
caller()
Обмазываем, запускаем, смотрим сводку:
ouroboros wrap-file halt.py
OUROBOROS_DEBUG_INFO=./halt.info python3 halt.py
ouroboros trace-stats ./halt.info
{
"ok": true,
"path": "halt.info",
"calls_parsed": 0,
"malformed": 0,
"total_calls": 0,
"in_flight": [
{
"name": "caller",
"call_id": "4adeb28e-010a-44e0-a080-8cd798fe06bc",
"started": "2026-09-10T19:59:33.724",
"cpu": null,
"thread": "19.128912376537600"
},
{
"name": "never_returns",
"call_id": "7d4d09a0-640d-4497-9645-6b34fcace314",
"started": "2026-09-10T19:59:33.724",
"cpu": null,
"thread": "19.128912376537600"
}
],
"by_function": [],
"by_thread": [],
"duration_seconds": null,
"timespan": null,
"note": "counts/durations are over completed calls; `duration_seconds` are REAL per-call durations (exit−entry) from each call's `d`. `by_thread` groups calls by the `th` token (CPUs each thread ran on); empty for traces with no thread field. `in_flight` = entered (`p:in`) but never completed. `timespan` is first→last entry time."
}
Завершённых вызовов ноль. Незавершённых — два, и названы оба: программа вошла в
caller, оттуда в never_returns и не вышла ни из одного.
Это не поломка трассы, а её ответ. Списка in_flight не было бы вовсе, если
бы записывалась одна строка на вызов, — писать её было бы просто некуда. Ради
этого случая записей и две.
Практически: увидели непустой in_flight — читайте его как «вот где оно
осталось». Самый глубокий вызов в списке и есть место, дальше которого программа
не прошла.
Отказ третий: в трассе оказалось не только записи
Разбор трассы устроен осторожно. Строка, которая не начинается со скобки { или
не разбирается как JSON, не роняет чтение — она считается битой и попадает в
счётчик malformed.
Добавим в трассу две посторонние строки и перечитаем:
cp debug.info debug.copy
printf 'не json\n{"p":"in"\n' >> ./debug.copy
ouroboros trace-stats ./debug.copy
Начало ответа — дальше идёт обычная сводка по функциям:
{
"ok": true,
"path": "debug.copy",
"calls_parsed": 9,
"malformed": 2,
"total_calls": 9,
"in_flight": [],
"by_function": [
Девять вызовов разобрано, две строки признаны битыми, чтение прошло. Так и задумано: трасса может обрываться на середине строки (программа упала в момент записи), и терять из-за этого весь остальной разбор было бы глупо.
Отдельно стоит знать, что не считается битым. Песочница дописывает в
debug.info служебную строку про каждую запущенную команду:
{"p":"exec","cmd":["python3","discount.py"],"rc":1,"out":"9999 False 9999.0\n10000 False 9000.0\n500 True 425.0\n","err":"Traceback (most recent call last):\n …\nTypeError: '>=' not supported between instances of 'str' and 'int'\n"}
Многоточие тут наше: в трассе поле err держит всю трассировку целиком.
Строка — правильный JSON, но она не про вызов функции. Разбор такие строки
пропускает молча и в malformed не считает — иначе счётчик битых строк рос
бы на каждом запуске и перестал бы что-либо значить.
Отсюда практическое чтение: malformed больше нуля — это про порчу, а не
про посторонние записи. Стоит посмотреть, что там за строки.
Чего в этом списке нет
Инструмент не отказывается записывать вызов из-за того, что довод показался ему странным, длинным или непонятным. Длинные значения он обрезает (у Python предел — 200 знаков на значение), но запись всё равно делает.
Это стоит держать в голове, потому что обрезанное значение выглядит как
значение. Если в трассе видно 'ааааа…' — возможно, там было гораздо больше.
Про это подробнее в главе про границы.
Итого
| отказ | что значит | что делать |
|---|---|---|
corrupted source |
файл не разобрался, записи не было | починить синтаксис |
непустой in_flight |
вызовы вошли и не вернулись | смотреть самый глубокий: там и встало |
malformed больше нуля |
в трассе строки, которые не разобрались | посмотреть, что за строки и откуда |
Дальше — что записывать и как найти нужное: обмазывать весь файл нужно не всегда, а девять тысяч записей глазами не читают.