#!/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/.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"