DigitableCourses
Настройки портала
Показать возможности портала

Локально и без аккаунта. Аккаунта нет, регистрация не нужна: введённое в инструменты остаётся в localStorage браузера и на сервер не уходит.

Свой счётчик считает открытия страниц и дочитывания: уезжает адрес и десятая доля текста. Без cookies и чужих счётчиков, IP не хранится, Do Not Track уважается. Как это проверить

Репозиторий портала не выложен, «открытым кодом» мы его не зовём. Открыто это:

Живёт портал на донатах, платных консультациях и разборах по запросу и покупке Workbench.

Планов делать курсы платными нет.

Уроборос

Запись всех вызовов работающей программы: какие функции звались, с чем их позвали, что они вернули и что бросили.

Настоящий прогон: пятнадцать строк, которые падают

$ ouroboros wrap-snippet -l python < discount.py

Кадр вывода команды ouroboros wrap-snippet -l python < discount.py: 24 строк моноширинного текста на тёмном фоне. Тот же вывод буквами лежит рядом, под «весь вывод текстом».
Подопытная программа целиком и то, что к ней дописано: ввоз помощника и @_ouro_log над каждой из трёх функций. Ничего не вычисляют — только записывают. wrap-file делает то же самое прямо в файле.
Весь вывод текстом
from ouroboros_runtime import log as _ouro_log
@_ouro_log
def discount_rate(total, member):
    if member:
        return 0.15
    if total >= 10000:
        return 0.10
    return 0.0


@_ouro_log
def apply_discount(total, member=False):
    rate = discount_rate(total, member)
    return round(total * (1 - rate), 2)


@_ouro_log
def main():
    for total, member in [(9999, False), (10000, False), (500, True)]:
        print(total, member, apply_discount(total, member=member))
    apply_discount("free")


main()

$ OUROBOROS_DEBUG_INFO=./debug.info python3 discount.py

Кадр вывода команды OUROBOROS_DEBUG_INFO=./debug.info python3 discount.py: 22 строк моноширинного текста на тёмном фоне. Тот же вывод буквами лежит рядом, под «весь вывод текстом».
Программа запускается как обычно и печатает своё. На четвёртом входе она падает, и обычная трассировка говорит, где, но не говорит, с чем позвали.
Весь вывод текстом
9999 False 9999.0
10000 False 9000.0
500 True 425.0
Traceback (most recent call last):
  File "/home/user/shop/discount.py", line 24, in <module>
    main()
    ~~~~^^
  File "/home/user/shop/ouroboros_runtime.py", line 234, in wrapper
    result = fn(*args, **kwargs)
  File "/home/user/shop/discount.py", line 21, in main
    apply_discount("free")
    ~~~~~~~~~~~~~~^^^^^^^^
  File "/home/user/shop/ouroboros_runtime.py", line 234, in wrapper
    result = fn(*args, **kwargs)
  File "/home/user/shop/discount.py", line 13, in apply_discount
    rate = discount_rate(total, member)
  File "/home/user/shop/ouroboros_runtime.py", line 234, in wrapper
    result = fn(*args, **kwargs)
  File "/home/user/shop/discount.py", line 6, in discount_rate
    if total >= 10000:
       ^^^^^^^^^^^^^^
TypeError: '>=' not supported between instances of 'str' and 'int'

$ grep fb17367a debug.info

Кадр вывода команды grep fb17367a debug.info: 2 строк моноширинного текста на тёмном фоне. Тот же вывод буквами лежит рядом, под «весь вывод текстом».
Две строки одного вызова, связанные общим номером: на входе — имя и доводы, на выходе — что вернули или бросили и сколько это заняло. Так выглядит debug.info.
Весь вывод текстом
{"p":"in","t":"2026-09-10T19:51:47.402","id":"fb17367a-f596-4198-9565-add097cefa0e","ci":-1,"th":"2.136045302235648","fn":"discount_rate","a":"'free', False","k":""}
{"p":"out","id":"fb17367a-f596-4198-9565-add097cefa0e","fn":"discount_rate","x":"TypeError: '>=' not supported between instances of 'str' and 'int'","d":3e-06}

$ ouroboros trace ./debug.info --outcome raised --function discount_rate

Кадр вывода команды ouroboros trace ./debug.info --outcome raised --function discount_rate: 26 строк моноширинного текста на тёмном фоне. Тот же вывод буквами лежит рядом, под «весь вывод текстом».
Тот же вызов, найденный по исходу и имени. Здесь видно то, чего в трассировке нет: позвали со строкой «free» вместо числа.
Весь вывод текстом
{
  "ok": true,
  "path": "debug.info",
  "calls_parsed": 9,
  "malformed": 0,
  "matched": 1,
  "returned": 1,
  "next_cursor": null,
  "in_flight": [],
  "in_flight_truncated": false,
  "records": [
    {
      "index": 6,
      "started": "2026-09-10T19:51:47.402",
      "call_id": "fb17367a-f596-4198-9565-add097cefa0e",
      "name": "discount_rate",
      "args": "'free', False",
      "kwargs": "",
      "outcome_kind": "raised",
      "outcome": "TypeError: '>=' not supported between instances of 'str' and 'int'",
      "duration": 3e-06,
      "cpu": null,
      "thread": "2.136045302235648"
    }
  ]
}

$ ouroboros trace-stats ./debug.info

Кадр вывода команды ouroboros trace-stats ./debug.info: 75 строк моноширинного текста на тёмном фоне. Тот же вывод буквами лежит рядом, под «весь вывод текстом».
Свод по всему прогону: сколько раз позвана каждая функция, сколько раз вернула и сколько бросила, и настоящая длительность каждого вызова.
Весь вывод текстом
{
  "ok": true,
  "path": "debug.info",
  "calls_parsed": 9,
  "malformed": 0,
  "total_calls": 9,
  "in_flight": [],
  "by_function": [
    {
      "name": "apply_discount",
      "count": 4,
      "result": 3,
      "raised": 1,
      "unknown": 0,
      "duration_seconds": {
        "min": 7e-05,
        "max": 0.000103,
        "mean": 8.7e-05,
        "total": 0.000347,
        "count": 4
      }
    },
    {
      "name": "discount_rate",
      "count": 4,
      "result": 3,
      "raised": 1,
      "unknown": 0,
      "duration_seconds": {
        "min": 1e-06,
        "max": 3e-06,
        "mean": 2e-06,
        "total": 7e-06,
        "count": 4
      }
    },
    {
      "name": "main",
      "count": 1,
      "result": 0,
      "raised": 1,
      "unknown": 0,
      "duration_seconds": {
        "min": 0.000693,
        "max": 0.000693,
        "mean": 0.000693,
        "total": 0.000693,
        "count": 1
      }
    }
  ],
  "by_thread": [
    {
      "thread": "2.136045302235648",
      "count": 9,
      "functions": 3,
      "cpus": []
    }
  ],
  "duration_seconds": {
    "min": 1e-06,
    "max": 0.000693,
    "mean": 0.000116,
    "total": 0.001047,
    "count": 9
  },
  "timespan": {
    "first": "2026-09-10T19:51:47.402",
    "last": "2026-09-10T19:51:47.402",
    "seconds": 0.0,
    "timestamps_parsed": 9,
    "timestamps_unparsed": 0
  },
  "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."
}

Это настоящий вывод выпуска v0.6.1, снятый прогоном 2026-09-10, а не набранный руками текст. Знаки — из прогона; добавлены шрифт и подсветка, а длинные строки перенесены по ширине окна. Весь вывод целиком лежит ниже текстом.

Запись вызовов, а не отладчик

Инструмент дописывает в ваш исходник строки, которые ничего не вычисляют, а только записывают случившееся. Программа после этого запускается как обычно, и каждый вызов оставляет две записи: одну при входе — имя функции и доводы, — вторую при выходе: результат или исключение и длительность. Обе связаны общим номером и копятся в файле debug.info, по объекту JSON в строке.

Восемь языков: Python, JavaScript/TypeScript, C, C++, Elixir, Go, Java, C#. Схема записи у всех одна; различается диалект, которым язык печатает доводы.

Читать записи можно двумя способами. Человеку — командная строка: отобрать вызовы по имени, исходу или длительности и свести весь прогон в счётчики. ИИ-агенту — сервер MCP: те же операции, отданные как инструменты.

Это не профилировщик — записи меняют время прогона. Не отладчик — программа не останавливается. И не покрытие: покрытие говорит, что строка исполнилась, а запись — с чем позвали и что вышло.

Поставить и позвать

Нужен Python 3.12 или новее. Любой из трёх способов кладёт на PATH две команды — ouroboros и ouroboros-mcp.

brew install digitable-lol/tap/ouroboros
asdf plugin add ouroboros https://github.com/digitable-lol/ouroboros.git
uv tool install git+https://github.com/digitable-lol/ouroboros
wrap-file <файл>

Обмазать файл целиком, прямо на месте. Рядом появляется помощник, который и пишет записи; больше ничего не добавляется.

wrap-functions <файл> <имена>

Обмазать только названные функции. Так поступают, когда трасса всего файла длиннее исходника в сотни раз.

trace <debug.info>

Отобрать вызовы: по имени, по исходу — вернул, бросил, не вернулся, — по длительности, по потоку.

trace-stats <debug.info>

Свести прогон в счётчики: сколько вызовов у каждой функции, чем кончились, сколько заняли и какие не вернулись.

Команд 17; здесь четыре, с которых начинают. Те же операции отданы ИИ-агенту сервером MCP — инструментов 17, порядок работы в приветствии сервера. Выпуск 0.6.1: проверок 1257, покрытие строк и ветвей 100,00 %.

Где инструмент останавливается

  • Записывает, как код себя вёл, а не как он должен: «на входе 10000 вышло 9000.0» — наблюдение, а не правило. Ошибка на границе попадёт в трассу как обычная запись.
  • О том, чего не случилось, трасса молчит и не предупреждает об этом: ветвь, в которую не зашли, не оставит ни строки. Молчание значит «здесь не были», а не «здесь всё в порядке».
  • Не профилировщик: обмазка меняет время прогона. Добавка на вызов — от 15,6 микросекунды у C# до 190,7 у Elixir, и дороже всего сама запись, а не язык.
  • Обмазка переписывает исходник, и обмазанный файл остаётся обмазанным — в общую ветку ему нельзя. Снять вставки инструмент не умеет: возврат к чистому коду делается системой контроля версий.

Курс из семи глав: установка, отказы, отбор вызовов, работа с агентом, восемь языков и границы

Исходники и выпуски