Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWeV2yiMWTjkeaQfSsYuL
165 lines
13 KiB
Bash
Executable file
165 lines
13 KiB
Bash
Executable file
#!/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"
|