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:
parent
b226c373a4
commit
14884ed802
2 changed files with 265 additions and 0 deletions
|
|
@ -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
133
eval/dovodka/narezka.py
Normal 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**, первая строка файла —
|
||||
ОБЯЗАТЕЛЬНО заголовок главы. Нарушение любого из трёх молча сдвигает нумерацию глав у движка.
|
||||
|
||||
⚠ **МИНА НУМЕРАЦИИ, названная до запуска.** Движок не умеет смещение глав: срез всегда нумеруется
|
||||
с единицы. Вырезав главы 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())
|
||||
Loading…
Add table
Reference in a new issue