textmachine/eval/dovodka/pachka.sh

165 lines
13 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# pachka.sh — интерливинг пробы по четырём осям: юнит за юнитом, пять клеток в порядке от сида.
#
# ⛔ ЧЕТЫРЕ МЕСТА, ГДЕ ЦИКЛ ИЗ §9.3 ПЛАНА СЛОМАЛСЯ БЫ МОЛЧА — здесь они исправлены, и каждое
# исправление записано в PREREG.md §9 как находка, а не как правка замороженного:
#
# 1. `rc=$?` ПОСЛЕ `| tee` возвращает код tee, а не tmctl ⇒ проверка «код 4» не сработала бы
# НИКОГДА. Здесь — ${PIPESTATUS[0]}.
# 2. `grep -q 'VOLUME CEILING:' && break 2` разорвал бы цикл после ПЕРВОЙ клетки: строка печатается
# при res.Volume != nil, то есть на КАЖДОМ прогоне с --max-units (cmd/tmctl/render.go:130).
# Здесь останов идёт по ОТСУТСТВИЮ строки «never been delivered» — она и означает LeftFresh > 0.
# 3. `--config $B/arms/$arm.yaml` — translate грузит book.yaml, а не pipeline. Здесь .book.yaml.
# 4. --ceiling-usd сравнивается с НАКОПЛЕННЫМ расходом книги (cmd/tmctl/main.go:260), а копия несёт
# базовый расход ⇒ в флаг кладётся «база + доля», а не «доля».
#
# ⛔ --resnapshot НЕ ДАЁТСЯ НИКОГДА: он перекупил бы все уже оплаченные клетки. Предупреждение
# планировщика о будущем --resnapshot печатается на каждом ограниченном прогоне и ожидаемо.
#
# Скрипт РЕЗЮМИРУЕМ: клетки, уже купленные, движок отдаёт из чекпойнтов за $0.
set -uo pipefail
B="${B:-$HOME/books/gu-zhenren/probe-4axes}"
REPO="${REPO:-/home/ubuntu/projects/textmachine}"
KEYS="${KEYS:-$REPO/backend/.env}" # ⚠ путь к ключам; содержимое НЕ читается никем, кроме движка
PY="$REPO/eval/.venv/bin/python"
SEED=20260902
SANCTION=9.50
N="${N:-20}" # из пре-рега (tmctl manifest, $0)
FROM_UNIT="${FROM_UNIT:-1}"
KEYS_REF=""
# Доли санкции — ЗАПИНЕНЫ ПРЕ-РЕГОМ. Сумма = 1.0000 ⇒ агрегат ровно $9.50 при любой базе.
declare -A W=( [p7-off]=0.30 [p9-off]=0.30 [p7-low]=0.1666666667 [p9-low]=0.1666666667 [glm-off]=0.0666666667 )
# ⚠ ЧТЕНИЕ ТОЛЬКО В RO: цикл ничего в базах не меняет, и открывать их RW незачем.
spend_of() { sqlite3 "file:$1?mode=ro" "select coalesce(sum(committed_usd+reserved_usd),0) from spend;" 2>/dev/null || echo 0; }
# ⛔ БАЗА БЕРЁТСЯ ИЗ ТОГО ФАЙЛА БЭКАПА, ИЗ КОТОРОГО СДЕЛАНЫ КОПИИ, а не с живой базовой БД.
# Причина: агрегат §9.5 есть «база + Σ(копия база)», и если живую базу кто-то тронет (или её
# тронет сам движок повторным прогоном), вычитаемое уедет — молча, и агрегат соврёт в обе стороны.
# BASE_SNAPSHOT_DB задаётся снаружи; по умолчанию — самый свежий бэкап.
BASE_DB="${BASE_SNAPSHOT_DB:-$(ls -1t "$B"/backups/*.db 2>/dev/null | head -1)}"
[ -z "$BASE_DB" ] && { echo "⛔ не найден бэкап базы в $B/backups — из чего сделаны копии?"; exit 1; }
BASE=$(spend_of "$BASE_DB")
# ⚠ ПОТРАЧЕНО ВНЕ ТЕКУЩИХ БАЗ. Деньги, ушедшие провайдеру из БД, которая потом была пере-создана,
# из леджера копии ИСЧЕЗАЮТ, а из счёта — нет. Агрегат «база + Σ(копия база)» их не увидит.
# Заведено после того, как $0.018243 ушли в p7-low на терминолога и рука была пере-копирована.
OUTSIDE="${OUTSIDE:-0.018243}"
aggregate() { # израсходовано = база + Σ(копия база) + потрачено вне баз
$PY - "$BASE" "$B" "$OUTSIDE" <<'PYEOF'
import sqlite3, sys, pathlib
base = float(sys.argv[1]); B = pathlib.Path(sys.argv[2]); tot = base + float(sys.argv[3])
for arm in ["p7-off","p7-low","p9-off","p9-low","glm-off"]:
p = B/"arms"/f"{arm}.db"
if not p.exists(): continue
con = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
s = con.execute("select coalesce(sum(committed_usd+reserved_usd),0) from spend").fetchone()[0]
con.close(); tot += s - base
print(f"{tot:.6f}")
PYEOF
}
ceiling_for() { $PY -c "print(f'{$BASE + $1*($SANCTION-$BASE):.4f}')"; }
echo "════════════════════════════════════════════════════════════════════════"
echo "ПАЧКА: юнитов $N, сид $SEED, санкция \$$SANCTION"
echo "база взята из $BASE_DB ⇒ \$$BASE · потрачено вне баз \$$OUTSIDE"
echo "════════════════════════════════════════════════════════════════════════"
# ⛔ ПЕРЕД ПЕРВОЙ КЛЕТКОЙ: каждая копия обязана нести РОВНО базовый расход. Копия, сделанная не из
# этого бэкапа (или тронутая), сломала бы агрегат так, что этого не было бы видно ни в одной сумме.
for arm in p7-off p7-low p9-off p9-low glm-off; do
S=$(spend_of "$B/arms/$arm.db")
# ⚠ После пред-полёта копия несёт базу ПЛЮС свои купленные клетки, поэтому равенства уже нет.
# Проверяется то, что осталось проверяемым: копия НЕ МОЖЕТ нести МЕНЬШЕ базы — иначе она сделана
# не из этого бэкапа, и вычитаемое в агрегате неверно.
if $PY -c "import sys; sys.exit(0 if $S + 1e-9 < $BASE else 1)"; then
echo "⛔ КОПИЯ $arm НЕСЁТ \$$S — МЕНЬШЕ базы \$$BASE. Сделана не из этого бэкапа. СТОП."
exit 1
fi
U=$(sqlite3 "file:$B/arms/$arm.db?mode=ro" "select count(*) from chunk_status where stage='edit';")
printf " %-8s леджер \$%s · edit-юнитов %s\n" "$arm" "$S" "$U"
done
echo "✓ все пять копий сделаны из бэкапа \$$BASE и несут свои клетки"
for (( u=FROM_UNIT; u<=N; u++ )); do
ORDER=$($PY "$REPO/eval/dovodka/poryadok.py" --unit "$u" --seed "$SEED") || { echo "⛔ порядок не получен"; exit 1; }
echo; echo "──── ЮНИТ $u/$N · порядок: $ORDER"
for arm in $ORDER; do
CEIL=$(ceiling_for "${W[$arm]}")
LOG="$B/logs/$arm.log"
CELL="$B/logs/$arm.u$u.log" # ⚠ отдельный лог клетки: tail общего может прочитать
# строку ПРЕДЫДУЩЕГО прогона; плюс он даёт время клетки
# ⛔ ПАУЗА КУПЛЕНА ИСПОЛНЕНИЕМ, А НЕ ОСТОРОЖНОСТЬЮ: `preflightBackup` (cmd/tmctl/backup.go) пишет
# точку восстановления в <каталог project_db>/backups/<ts>.db с точностью до СЕКУНДЫ и ОТКАЗЫВАЕТ
# стартовать, если файл уже есть («refusing to overwrite a restore point»). Пять рук лежат в одном
# каталоге, и запуск двух в одну секунду роняет вторую кодом 1 ещё до всякой работы. Поймано на
# $0-гарде оси: три руки из пяти упали именно так. В пачке это остановило бы прогон на сотне клеток.
sleep 2
echo "$arm (--ceiling-usd $CEIL) $(date -u +%H:%M:%SZ)"
# ⚠ Канал сырых тел включается ТОЛЬКО на первом юните пачки: у референса `glm-off` клетка
# юнита 1 была куплена $0-гардом БЕЗ канала, и его проводная улика не снята. Одного юнита
# достаточно; дальше канал выключен, чтобы логи не росли на сотне клеток.
BODIES=""
[ "$u" = "$FROM_UNIT" ] && BODIES="1"
LOG_LLM_BODIES="$BODIES" LOG_LEVEL="$([ -n "$BODIES" ] && echo debug || echo info)" \
"$B/bin/tmctl" translate \
--config "$B/arms/$arm.book.yaml" \
--keys-file "$KEYS" \
--verify-bank \
--max-units 1 \
--ceiling-usd "$CEIL" 2>&1 | tee "$CELL" | tee -a "$LOG"
rc=${PIPESTATUS[0]} # ⛔ не $? — тот вернул бы код tee
case $rc in
0|2) ;; # 0 чисто · 2 завершено с флагами — оба штатные
3) echo "⛔ КОД 3 (стоп подписи банка) В РУКЕ $arm — этого быть не должно: банк уже предъявлен"
echo " базовым прогоном и копии его несут. СТОП, вопрос владельцу."; exit 3 ;;
4) echo "⛔ КОД 4 (денежный отказ) В РУКЕ $arm — потолок. СТОП и вопрос владельцу;"
echo " добор после просмотра чисел запрещён."; exit 4 ;;
*) echo "⛔ НЕОЖИДАННЫЙ КОД $rc в руке $arm — СТОП."; exit "$rc" ;;
esac
# ⛔ Останов по РАБОТЕ, а не по строке VOLUME CEILING: она печатается всегда.
if ! grep -q 'never been delivered' "$CELL"; then
echo "$arm: недоставленных юнитов не осталось — рука закончена"
fi
# ⛔ ИНВАРИАНТ СИНХРОННОСТИ РУК. Блокировка трудности держится на том, что k-я клетка — ОДИН И
# ТОТ ЖЕ юнит у всех пяти рук. Оборванный вызов без чекпойнта сдвинул бы одну руку на юнит
# назад, и с этого места руки сравнивали бы РАЗНЫЕ чанки — молча, без единой ошибки.
DB="$B/arms/$arm.db"
KEYS_NOW=$(sqlite3 "file:$DB?mode=ro" "select coalesce(group_concat(k),'') from (select chapter||':'||chunk_idx k from chunk_status where stage='edit' order by chapter, chunk_idx);" 2>/dev/null || echo ERR)
N_NOW=$(sqlite3 "file:$DB?mode=ro" "select count(*) from chunk_status where stage='edit';" 2>/dev/null || echo -1)
AFTER=$(spend_of "$DB")
printf " юнитов редактуры: %s (ждём %s) · леджер руки \$%s · ключи %s\n" "$N_NOW" "$u" "$AFTER" "$KEYS_NOW"
if [ "$N_NOW" != "$u" ]; then # юнит 1 куплен пред-полётом, поэтому счёт совпадает с номером
echo "РУКА $arm РАЗЪЕХАЛАСЬ: редакторских юнитов $N_NOW вместо $u."
echo " Дальше руки сравнивали бы РАЗНЫЕ юниты — блокировка трудности сломана. СТОП."
exit 5
fi
# ⛔ СВЕРКА ПО НАБОРУ КЛЮЧЕЙ, А НЕ ПО СЧЁТЧИКУ: счётчик совпал бы и у рук, купивших РАЗНЫЕ юниты
# (движок мог бы выбрать другой порядок), и рассинхрон прошёл бы молча.
if [ -z "${KEYS_REF:-}" ]; then KEYS_REF="$KEYS_NOW"; fi
if [ "$KEYS_NOW" != "$KEYS_REF" ]; then
echo "РУКА $arm КУПИЛА ДРУГИЕ ЮНИТЫ:"
echo " эта рука : $KEYS_NOW"
echo " эталон : $KEYS_REF"
echo " Блокировка трудности сломана — руки сравнивают РАЗНЫЙ материал. СТОП."
exit 5
fi
done
KEYS_REF="" # эталон набора ключей задаётся ПЕРВОЙ рукой каждого юнита
SPENT=$(aggregate)
echo " ── агрегат после юнита $u: \$$SPENT из \$$SANCTION"
if $PY -c "import sys; sys.exit(0 if $SPENT >= $SANCTION else 1)"; then
echo "⛔ АГРЕГАТ ДОСТИГ САНКЦИИ — СТОП и вопрос владельцу. Добор запрещён."; exit 4
fi
done
echo; echo "ПАЧКА ЗАВЕРШЕНА. Агрегат: \$$(aggregate) из \$$SANCTION"