Turn the plan into a procedure: the fresh-eyes check found it unbuildable without invention, so the stand, the arms, the commands, the exit codes and the manual ceiling aggregate are now written down

This commit is contained in:
heaven 2026-09-02 00:14:27 +03:00
parent b226c373a4
commit 14884ed802
2 changed files with 265 additions and 0 deletions

View file

@ -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`.** Промпт резолвится
конвенцией `<prompts_root>/<пара>/<роль>.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` — генератор порядка рук от сида.

133
eval/dovodka/narezka.py Normal file
View file

@ -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**, первая строка файла
ОБЯЗАТЕЛЬНО заголовок главы. Нарушение любого из трёх молча сдвигает нумерацию глав у движка.
**МИНА НУМЕРАЦИИ, названная до запуска.** Движок не умеет смещение глав: срез всегда нумеруется
с единицы. Вырезав главы 2642, в базе и отчётах увидишь «главы 117». Соответствие держится
ТОЛЬКО этим файлом и строкой в пре-реге. Поэтому скрипт печатает обе нумерации и пишет их в
манифест рядом с 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("первая строка среза 2642 — заголовок", 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())