diff --git a/eval/dovodka/PLAN-01-09.md b/eval/dovodka/PLAN-01-09.md index 3699bf10..8308ac20 100644 --- a/eval/dovodka/PLAN-01-09.md +++ b/eval/dovodka/PLAN-01-09.md @@ -273,3 +273,135 @@ n=16, sd 0.0069**. Ошибка типично ~3% видимого выхода факт о мире. Лечение, выведенное из десяти случаев: **отрицательный результат предъявляется с положительным контролем**, а прежде «этого нет» — смотреть каталог артефактов, а не только доки своей ветки. + +--- + +# 9. ПРОЦЕДУРА — КОМАНДЫ, А НЕ АРГУМЕНТЫ + +> ⚠ **Этот раздел дописан 02.09 после проверки исполнимости свежей сессией.** Её вердикт был: +> «план написан как аргумент, а не как процедура — из него нельзя набрать ни одной команды», и +> объём домысливания оказался больше, чем то, что план задаёт. Все якоря при этом были верны: +> дефект был не в фактуре, а в форме. Ниже — то, чего не хватало. + +## 9.0 ⛔ ТРИ МЕСТА, ГДЕ ДОМЫСЛИВАНИЕ СТОИТ ДЕНЕГ ИЛИ ЗАМЕРА + +Прочти их ПЕРВЫМИ; каждое проверено исполнением. + +1. **`--verify-bank` НЕ ГАРАНТИРУЕТ останова.** Флаг единственный, кто вообще может остановить + прогон до редакторской волны, но при ПУСТОЙ дельте майнинга движок авто-продолжает + (`mining.go:152-164`, флаг лишь повышает уровень лога). Если это случится, базовый прогон купит + редакторские юниты, и каждая из четырёх копий потребует пересборки снапшота с перекупкой. + ⇒ **ПОЯС:** базовому прогону ставится `--ceiling-usd` чуть выше ожидания ЧЕРНОВОЙ волны, чтобы + редакторская покупка упёрлась в денежный отказ (код 4), а не прошла молча. +2. **Ось промпта реализуется ТОЛЬКО через `stages[edit].prompt_override`.** Промпт резолвится + конвенцией `/<пара>/<роль>.md`, а `prompts_root` живёт в `pairs/zh-ru.yaml`, + общем для всех конфигов каталога. ⇒ без `prompt_override` четыре руки схлопнутся в две: + **платишь за четыре, меришь две**. Ловится гардом «четыре разных `snapshot_id`» — снимать его + запросом к `snapshots` каждой из четырёх баз. +3. **Потолка $8.6 не существует как механизма.** Леджер считает по `book_id`, а таблица `spend` + лежит ВНУТРИ проектной базы: четыре копии сторожат каждая себя и каждая уже несёт базовый + расход. ⇒ **агрегат ведётся вручную** (§9.5), и санкционированную сумму держит только он. + +## 9.1 СТЕНД + +**Образец — `~/books/gu-zhenren/coldrun-v16/`** (его `book.yaml`, `pipeline.yaml`, `pairs/zh-ru.yaml`). +⚠ Каталог берётся как ОБРАЗЕЦ, но не как рабочее место: в нём три оплаченных редакторских юнита, +которые заставят пересборку снапшота на каждой копии. + +⛔ **Пути на `models`, `langpack_root`, `prompts_root` обязаны смотреть в +`/home/ubuntu/projects/textmachine-main/`**, а не в своё дерево: **в `polygon` нет +`backend/configs/langpacks/ru`** (проверено: там только `zh`, `zh-ru`), а загрузчик на отсутствующий +каталог молча возвращает умолчания — русский слой читателя потеряется во всех четырёх руках тихо. +`backend/prompts` и `models.yaml` в двух деревьях побайтно совпадают, расходится только `langpacks`. + +```bash +B=~/books/gu-zhenren/probe-4axes # новый каталог, не coldrun-v16 +mkdir -p $B/{bin,logs,arms} +cp ~/books/gu-zhenren/coldrun-v16/{book.yaml,pipeline.yaml} $B/ +cp -r ~/books/gu-zhenren/coldrun-v16/pairs $B/ +# исходник: 17 глав, вариант A +eval/.venv/bin/python eval/dovodka/narezka.py --from 26 --to 42 --out $B/guzhenren-ch26-42.gb18030.txt +# бинарь — из main-дерева, sha пинится в пре-рег +cd /home/ubuntu/projects/textmachine-main/backend && go build -o $B/bin/tmctl ./cmd/tmctl +sha256sum $B/bin/tmctl +``` + +## 9.2 ЧЕТЫРЕ РУКИ + +Четыре копии `pipeline.yaml` в `$B/arms/`, различающиеся РОВНО двумя строками стадии `edit`: + +| файл | `prompt_override` | `reasoning` | +|---|---|---| +| `p7-off.yaml` | `$B/editor-p7.md` | `"off"` | +| `p7-low.yaml` | `$B/editor-p7.md` | `"low"` | +| `p9-off.yaml` | `$B/editor-p9.md` | `"off"` | +| `p9-low.yaml` | `$B/editor-p9.md` | `"low"` | + +```bash +cd /home/ubuntu/projects/textmachine +git show e89874b^:backend/prompts/zh-ru/editor.md > $B/editor-p7.md # июльский +git show HEAD:backend/prompts/zh-ru/editor.md > $B/editor-p9.md # сегодняшний +sha256sum $B/editor-p7.md $B/editor-p9.md # ОБА в пре-рег +``` + +⚠ **В образце `coldrun-v16/pipeline.yaml` комментарий утверждает, что у `pro` ручки эффорта нет +вовсе** — это протухло. Копируя файл, комментарий поправить или удалить, иначе следующий читатель +получит противоречие своему же плану. + +## 9.3 ПОРЯДОК + +**Коды выхода, без которых штатный стоп читается как падение:** `0` — чисто · `2` — завершено с +флагами · **`3` — стоп подписи банка** (`WaveSignatureStop`) · **`4` — денежный отказ** +(`CeilingHalt`). Объёмный стоп приходит и как `0`, и как `2`; различает только строка +`VOLUME CEILING:` в выводе. + +```bash +# 1. БАЗОВЫЙ ПРОГОН: черновая волна + майнинг + терминолог, стоп ДО редактуры +$B/bin/tmctl translate --config $B/book.yaml --verify-bank --ceiling-usd 1.20 2>&1 | tee $B/logs/base.log +# ожидаемый код 3 (стоп подписи). Код 0/2 ⇒ ⛔ СТОП: дельта была пуста, редактура куплена — +# к владельцу, не продолжать. + +# 2. КОПИИ — только через backup, не cp (живые -wal/-shm теряют данные) +$B/bin/tmctl backup --config $B/book.yaml --out $B/arms/p7-off.db +# …и так все четыре; в каждой копии свой project_db, pipeline и ceilings + +# 3. ИНТЕРЛИВИНГ: юнит за юнитом, руки в порядке из сида +for u in $(seq 1 $N); do + for arm in $(eval/.venv/bin/python eval/dovodka/poryadok.py --unit $u --seed 20260902); do + $B/bin/tmctl translate --config $B/arms/$arm.yaml --max-units 1 --ceiling-usd <своя> \ + 2>&1 | tee -a $B/logs/$arm.log + rc=$?; grep -q 'VOLUME CEILING:' $B/logs/$arm.log && break 2 + [ $rc -eq 4 ] && { echo "⛔ ПОТОЛОК — СТОП, вопрос владельцу"; exit 4; } + done +done +``` + +⛔ **Увидел требование `--resnapshot` — СТОП, не давать флаг.** Он перекупит все уже оплаченные +клетки. ⚠ Предупреждение планировщика о будущем `--resnapshot` на майнящей книге печатается на +КАЖДОМ ограниченном прогоне (десятки раз) — оно ожидаемо и не есть требование. + +## 9.4 РАНДОМИЗАЦИЯ + +Порядок рук внутри юнита — детерминированная перестановка от сида; **сид объявляется в пре-реге** +(`20260902`), выпавший порядок пишется в манифест. Без запиненного сида пре-рег не является +пре-регом. + +## 9.5 АГРЕГАТ ПОТОЛКА — ВРУЧНУЮ, ПОТОМУ ЧТО МЕХАНИЗМА НЕТ + +```bash +for db in $B/arms/*.db $B/*.db; do + echo -n "$(basename $db): "; sqlite3 "$db" "select round(sum(committed_usd+reserved_usd),4) from spend;" +done +``` +База считается ОДИН раз (она в каждой копии), поэтому: **израсходовано = база + Σ(копия − база)**. +Санкция владельца — **$8.6 в пиковых терминах**; при достижении — стоп и вопрос, добор запрещён. + +## 9.6 ЧЕГО ЕЩЁ НЕТ И ЧТО ПИСАТЬ ПЕРЕД ФРИЗОМ + +* **скрипт эндпойнта** (`chetyre.py`): ключ юнита, реконструкция попытки 0 (в `request_log` нет + колонки `attempt` — она в `checkpoints`), определение `delivered`, восстановление размышления, + правило цензуры, Вилкоксон с `zero_method='pratt'`; +* ⚠ **медиану и доверительный интервал Ходжеса–Лемана придётся писать руками**: `scipy 1.18.0` + установлен и `pratt` поддерживает, но интервала у `wilcoxon` не отдаёт (проверено); +* ⚠ **ковариатная модель не запустится**: `statsmodels` в `eval/.venv` не установлен; +* `poryadok.py` — генератор порядка рук от сида. diff --git a/eval/dovodka/narezka.py b/eval/dovodka/narezka.py new file mode 100644 index 00000000..e86a1541 --- /dev/null +++ b/eval/dovodka/narezka.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""narezka.py — вырезать диапазон глав книги в формат, который принимает движок. + +Заведён 02.09 по находке проверки исполнимости: пре-рег требовал «срез исходника (диапазон глав, +файл, его sha256)», а СПОСОБА получить этот файл в проекте не было ни одного. Резалки не +существовало, и свежая сессия обязана была бы её изобрести — вместе с форматом. + +ФОРМАТ, СНЯТЫЙ С ЭТАЛОНА (`~/books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt`, +проверено `file` и `od`): кодировка **GB18030**, переводы строки **CRLF**, первая строка файла — +ОБЯЗАТЕЛЬНО заголовок главы. Нарушение любого из трёх молча сдвигает нумерацию глав у движка. + +⚠ **МИНА НУМЕРАЦИИ, названная до запуска.** Движок не умеет смещение глав: срез всегда нумеруется +с единицы. Вырезав главы 26–42, в базе и отчётах увидишь «главы 1–17». Соответствие держится +ТОЛЬКО этим файлом и строкой в пре-реге. Поэтому скрипт печатает обе нумерации и пишет их в +манифест рядом с sha256. + +⚠ **ТОМА.** В книге нумерация глав СБРАСЫВАЕТСЯ по томам — `第二十六节` встречается несколько раз. +Поэтому режем по ПОРЯДКОВОМУ НОМЕРУ вхождения заголовка от начала файла, а не по тексту заголовка. + +Использование: + python3 eval/dovodka/narezka.py --from 26 --to 42 --out <путь>.gb18030.txt + python3 eval/dovodka/narezka.py --selftest +""" + +import argparse +import hashlib +import re +import sys +from pathlib import Path + +SRC = Path.home() / "books/gu-zhenren/guzhenren-utf8.txt" +HEAD_RE = re.compile(r"^第[^\n]{1,20}节") + + +def split_chapters(text): + """[(порядковый номер от 1, заголовок, тело с заголовком)] — по порядку вхождения, не по тексту.""" + lines = text.split("\n") + starts = [i for i, ln in enumerate(lines) if HEAD_RE.match(ln)] + out = [] + for k, i in enumerate(starts): + j = starts[k + 1] if k + 1 < len(starts) else len(lines) + out.append((k + 1, lines[i], "\n".join(lines[i:j]))) + return out + + +def cut(src_path, first, last, out_path): + text = src_path.read_text(encoding="utf-8") + chapters = split_chapters(text) + if not chapters: + print("⛔ ОТКАЗ: в исходнике не найдено ни одного заголовка главы — проверь путь и кодировку") + return 2 + if last > len(chapters): + print(f"⛔ ОТКАЗ: запрошена глава {last}, а в исходнике их {len(chapters)}") + return 2 + picked = chapters[first - 1 : last] + body = "\n".join(ch[2] for ch in picked) + if not HEAD_RE.match(body.split("\n")[0]): + print("⛔ ОТКАЗ: первая строка среза не заголовок — движок сдвинет нумерацию") + return 2 + data = body.replace("\r\n", "\n").replace("\n", "\r\n").encode("gb18030", errors="strict") + out_path.write_bytes(data) + sha = hashlib.sha256(data).hexdigest() + + print(f"срез: главы книги {first}–{last} → в файле пронумеруются 1–{len(picked)}") + print(f" файл : {out_path}") + print(f" байт : {len(data)}") + print(f" sha256 : {sha}") + print(f" заголовков в срезе: {sum(1 for ch in picked if HEAD_RE.match(ch[1]))}") + print(f" первая строка: {picked[0][1][:60]}") + print(f" последняя глава: {picked[-1][1][:60]}") + print() + print("⚠ В пре-рег занести ИМЕННО ЭТУ пару: диапазон книги и sha256 файла.") + print(f" главы книги {first}–{last} ≡ главы среза 1–{len(picked)}, sha256 {sha}") + return 0 + + +def selftest(): + """Гейты на самом скрипте: без них резалка может молча отдать не тот формат.""" + ok = True + + def check(name, cond): + nonlocal ok + print((" ✓ " if cond else " ✗ ") + name) + ok = ok and cond + + print("СЕЛФТЕСТ narezka.py") + check("исходник на месте", SRC.exists()) + if SRC.exists(): + chapters = split_chapters(SRC.read_text(encoding="utf-8")) + check(f"глав в исходнике > 2000 (найдено {len(chapters)})", len(chapters) > 2000) + # ⚠ Тест первой редакции проверял повтор ЦЕЛОЙ строки заголовка и был красным: строки + # уникальны, потому что несут НАЗВАНИЕ главы («第一节:纵身亡魔心仍不悔»). Повторяется + # НОМЕР — вот его и проверяем, ради этого и режем по порядку вхождения. + nums = [re.match(r"^第[^::]{1,12}节", c[1]).group(0) for c in chapters + if re.match(r"^第[^::]{1,12}节", c[1])] + check(f"НОМЕР главы в исходнике повторяется (тома): {len(nums)} заголовков, " + f"{len(set(nums))} различных номеров", len(set(nums)) < len(nums)) + body = "\n".join(c[2] for c in chapters[25:42]) + check("первая строка среза 26–42 — заголовок", bool(HEAD_RE.match(body.split("\n")[0]))) + try: + enc = body.replace("\n", "\r\n").encode("gb18030") + check("срез кодируется в GB18030 без потерь", True) + check("CRLF на месте", b"\r\n" in enc) + except UnicodeEncodeError as e: + check(f"срез кодируется в GB18030 без потерь — ОШИБКА {e}", False) + ref = Path.home() / "books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt" + if ref.exists(): + raw = ref.read_bytes() + check("эталон холодного прогона тоже CRLF", b"\r\n" in raw) + head = raw[:60].decode("gb18030", errors="replace") + check("эталон начинается с заголовка", bool(HEAD_RE.match(head))) + print("\n" + ("РЕЗАЛКА ГОДНА" if ok else "⛔ РЕЗАЛКА НЕ ГОДНА")) + return 0 if ok else 1 + + +def main(): + ap = argparse.ArgumentParser(description="вырезать диапазон глав в формат движка") + ap.add_argument("--from", dest="first", type=int, help="первая глава книги (сквозной номер)") + ap.add_argument("--to", dest="last", type=int, help="последняя глава книги включительно") + ap.add_argument("--out", type=Path, help="кудаписать срез") + ap.add_argument("--src", type=Path, default=SRC) + ap.add_argument("--selftest", action="store_true") + a = ap.parse_args() + if a.selftest: + return selftest() + if not (a.first and a.last and a.out): + ap.error("нужны --from, --to и --out (или --selftest)") + return cut(a.src, a.first, a.last, a.out) + + +if __name__ == "__main__": + sys.exit(main())