alchemistry

Инструменты

Чистый Python, без зависимостей. Нужны, чтобы не считать пропорции в уме и
чтобы CI ловил опечатки. Полный список команд перед коммитом — в корневом
файле AGENTS.md.

Рецепты лежат в ../recipes/ в JSON, чтобы их можно было читать
и как рецепт, и считать программно.

recalc.py — пересчёт пропорций

bash
python tools/recalc.py recipes/chocolate/dark-70.json                    # как есть
python tools/recalc.py recipes/chocolate/dark-70.json --scale 2          # вдвое больше
python tools/recalc.py recipes/twix/caramel.json --yield 500             # ровно 500 г карамели
python tools/recalc.py recipes/chocolate/milk.json --table md --share    # таблица с долями
python tools/recalc.py --list recipes/                                   # список рецептов
python tools/recalc.py --validate recipes/                               # проверка
echo '{"name":"Тест","ingredients":[{"name":"Мука","amount":100}]}' \
  | python tools/recalc.py - --scale 3                                   # через stdin

--yield считает множитель от поля yield (выход готового изделия), а если его
нет — от суммы ингредиентов. У карамели сырья 422 г, а на выходе 300 г, поэтому
«500 граммов» должно означать 500 г готовой карамели, а не сырья.

--table: txt (по умолчанию), md — вставить в документ или issue, csv —
для таблицы. --share добавляет долю каждого ингредиента.

--validate проверяет: битый JSON; пустой или отсутствующий ingredients;
пропущенные name и amount; отрицательные количества; повторяющиеся
ингредиенты с указанием позиций; нулевые количества у всех сразу; расхождение
суммы с yield больше чем на 12 % (считается от суммы; при allow_loss: true
проверка отключается целиком — у карамели и сиропов выпаривание законно).

shopping_list.py — список покупок

Суммирует ингредиенты из нескольких рецептов, убирает дубликаты, показывает из
чего каждый нужен. Для производственного дня —
../docs/base/batch-day.md.

bash
python tools/shopping_list.py \
  recipes/chocolate/dark-70.json:4 \
  recipes/chocolate/milk.json:3 \
  recipes/twix/wafer-short-dough.json:2 \
  recipes/twix/caramel.json:2

python tools/shopping_list.py --table md ... # для issue
python tools/shopping_list.py --no-sources ... # компактнее

Множитель задаётся через двоеточие: путь:4.

Ингредиент считается обязательным, если он обязателен хотя бы в одном
рецепте: какао-масло необязательно в тёмном шоколаде, но обязательно в молочном —
значит, в список оно попадёт как обязательное. Остальные необязательные
("optional": true) выносятся в раздел «По желанию» и в итог не входят.

Проверки

КомандаЧто ловит
python tools/check_links.pyБитые относительные ссылки; подсказывает, где файл на самом деле. Внешние ссылки и якоря пропускает. Открытые задачи на ненаписанные разделы помечаются как ошибки — это список дел
python tools/check_no_emoji.pyЭмодзи в текстах. Правило живёт в AGENTS.md
python tools/check_alchemy.pyРасхождение связей карты с составом рецептов. Схема рисует утверждение: если она говорит «горький → молочный», читатель поверит и купит горький
python tools/check_variants.pyРасхождение списка вариантов в data/alchemy.json с блоками ::: variant в статье
python tools/site/check_site.pyБитые ссылки и ассеты в собранном сайте, остатки markdown, несуществующие docUrl и recipeUrl в данных карты
python tools/site/verify_build.pyПобайтовую воспроизводимость сборки site/

Формат рецепта

json
{
 "name": "Горький шоколад 70 %",
 "description": "Базовый рецепт крафта из какао-тертого.",
 "yield": 510,
 "yield_unit": "г",
 "doc": "docs/chocolate/recipes/dark-70.md",
 "tags": ["шоколад", "горький"],
 "ingredients": [
 { "name": "Какао-тертое", "amount": 350, "note": "негорький сорт" },
 { "name": "Сахар", "amount": 150 },
 { "name": "Какао-масло", "amount": 10, "optional": true }
 ],
 "notes": ["Темперирование: 45-50 °C → 27-28 °C → 31-32 °C"]
}
ПолеОбязателенЗначение
nameдаНазвание рецепта
ingredientsдаСписок объектов, непустой
ingredients[].nameдаНазвание ингредиента
ingredients[].amountдаКоличество в граммах
ingredients[].note—Пояснение
ingredients[].section—Группировка («Тесто», «Начинка»)
ingredients[].optional—Помечает как необязательный
description—Подзаголовок в выводе
yield—Выход по рецепту
yield_unit—Единица выхода, по умолчанию г
allow_loss—Разрешает расхождение суммы с yield больше 12 %
checked—Автор сайта приготовил рецепт, и получилось вкусно. Метка «проверено» в списке
doc—Страница, где расписан рецепт: docs/...md. На неё ссылается панель карты
tags—Для поиска
notes—Заметки в конце вывода

CI

.github/workflows/ci.yml гоняет все проверки выше
на каждом PR и каждом пуше в main, плюс дымовой тест утилит и побайтовое
сравнение site/ со сборкой из текущих markdown.

Перед коммитом хотя бы:

bash
python tools/recalc.py --validate recipes/ && python tools/check_links.py