diff --git a/docs/experiments/25-door-to-file-b.md b/docs/experiments/25-door-to-file-b.md new file mode 100644 index 00000000..f0c2f246 --- /dev/null +++ b/docs/experiments/25-door-to-file-b.md @@ -0,0 +1,208 @@ +# Эксперимент 25. Холодный прогон B — «от двери до ФАЙЛА» во ВТОРОЙ раз, и впервые с вычиткой + +> **Пре-регистрация.** Всё, что ниже строки «ГРАНИЦА ФРИЗА», написано ДО первого платного вызова и +> закоммичено фриз-коммитом. Результаты дописываются ПОСЛЕ прогона отдельной секцией и ничего в +> пре-реге не правят: расхождение НАЗЫВАЕТСЯ, а не подгоняется. + +**Зона:** сквозной прогон · **Пак:** `docs/COLD_RUN_B_SESSION_PROMPT.md` (редакция `3fce077`) · **Дата:** 11.09.2026 +**Предмет:** четыре вещи, которых нет ни у кого, плюс заказ владельца на ВЫЧИТКУ боевого текста. + +## 0. Что именно покупается, и чего этот прогон НЕ покупает + +**Покупается:** (1) первое исполнение двух миграций схемы НА ПЛАТНОМ ПРОГОНЕ · (2) числа +консистентности С ПРОГОНА, а не пере-сканом сохранённой базы A · (3) платформенный пак СКВОЗЬ · +(4) пре-регистрированный ЗНАМЕНАТЕЛЬ денежной дыры и проверка её ЗНАКА · (5) ⭐ вычитка ВСЕГО +перевода против ВСЕГО оригинала отдельным опус-читателем и сверка ПРИБОРОВ движка с тем, что он увидел. + +⛔ **НЕ покупается и не оплачивается ни центом:** ответ на «безопасно ли редактору думать меньше» +(ряд 433) — это парный дизайн, отдельные деньги, отдельный заказ. B идёт на боевых настройках как есть. +⛔ **НЕ покупается ВЕЛИЧИНА доли выброшенного.** B — ЕЩЁ ОДИН одиночный прогон. Квирки §3д: при sd +логарифма 0.82 парный дизайн ловит эффект ×2.5 за **13 пар**, ×2.0 за 22, ×1.4 за 93. **n = 2 этой +нормы не выполняет и выполнить не может.** Писать «25.4 % подтверждено» по двум розыгрышам — ошибка. + +## 1. ГРАНИЦА ФРИЗА — что зафиксировано до первого цента + +| Предмет | Значение | +|---|---| +| Фриз-sha (КОД, которым собраны бинари) | `baa06cef7b2b30b8f7cbe031f3d0fabc03a01a55` | +| Фриз-дерево | локальный **клон** `~/tm-coldrun-b/freeze` (0 грязных строк; контроль — главное дерево в тот же момент 1) | +| Бинари | `tmctl` · `tmplatformd` · `tmplatformctl` · `stub` · `zeropipe` — **все пять** из клона, у каждого `vcs.revision=baa06ce…`, `vcs.modified=false`, строк `vcs.*` = 3. Негативный контроль: та же сборка из ГЛАВНОГО дерева даёт `vcs.modified=true`. ⚠ Прогон A объявил девиацией, что два его инструмента собраны НЕ из фриза (§12.5) — здесь эта девиация закрыта построением | +| Исходник (оригинал) | `books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt`, 57838 байт, sha256 `0b5f9b0266d8c32ac717a41211d2ed15d4f2a07f37a701fb686e255ab6c89f37` | +| Исходник (СРЕЗ, что покупается) | главы 1–3, **17564 байта**, sha256 `ed870ba6065ce3eb4eec12e80238efbc2be8511f38334ab1d92d5693f4ef4df9`. Нарезан МНОЙ из оригинала (`head -c`), байт-в-байт равен срезу прогона A (`cmp`). **Граница чистая:** декодируется целиком в **9021 знак**, кончается на границе абзаца, несёт **ровно 3** заголовка `第…节` при **10** в целом файле (контроль напечатан рядом) | +| Фактический pipeline-YAML | `~/tm-coldrun-b/mirror/cfg/pipeline-c1.yaml`, sha256 **`a46b33ee65b7713b4fbf86f64c788ba6b11eb0a219170c145a6205088156ad49`**, байт-в-байт равен `backend/configs/pipeline-c1.yaml` фриза (`cmp`) | +| Реестр моделей | `~/tm-coldrun-b/mirror/cfg/models.yaml`, sha256 `835af7b2d8830e6058b650ab742603d228febbd663978d88e0fe496ff78b5c1c` | +| Артефакт контраста | `mining-contrast.zh.txt` = jieba 0.42.1 `dict.txt`, sha256 `7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8`, 349 046 строк. ⚠ `CheckMiningContrast` делает ТОЛЬКО `os.Stat` и удовлетворился бы фальшивым кандидатом — sha сверена руками | +| Шаблон книги | `~/tm-coldrun-b/book-template-c1.yaml`, sha256 `4118a43059dba64a45290db73252a1306af341ff4c429100314256c95ca2f24d` | +| Потолки книги (ДВИЖКОВЫЕ, в шаблоне) | `book_usd: 1.25` · `day_usd: 2.50` — те же, что у A, ради сравнимости | +| Потолок ПЛАТФОРМЫ | холд ≈$2.24 (формула ниже), грант учётки **$2.50** | +| Стоп-правило СЕССИИ | факт **$1.00** — раньше обоих потолков; достигнут ⇒ СТОП и пинг, не решение сессии | + +### 1.1 ⭐ ДВЕ КАРТЫ СРАВНИМОСТИ A↔B, которых у прогона A не было + +1. **ПОКУПАЮЩИЙ файл тождествен.** sha `a46b33ee…` — та же, что в пре-реге прогона A. Что именно + покупается, между прогонами не изменилось ни на байт. +2. **ЦЕНОВАЯ МОДЕЛЬ тождественна.** `models.yaml` фриза A и фриза B различаются **ровно на 10 строк, + и все десять — КОММЕНТАРИИ** (не-комментарных изменённых строк: **0**). ⇒ долларовые числа A и B + считаны одним прайсом и сравнимы напрямую. +3. ⛔ **А КОД — НЕТ, и это записывается ОГРАНИЧЕНИЕМ, а не замалчивается.** Между `b0f5d89` (фриз A) и + `baa06ce`: **118 коммитов**, `backend/` **52 файла** (+7678/−85), `platform/` **30 файлов** + (+3945/−158), включая денежный путь. **B меряет ДРУГОЙ движок.** Прямой строкой: + **двойная оплата могла измениться из-за НАШИХ правок, а не из-за провайдера.** + +### 1.2 Срез по $0-манифесту (`tmctl manifest --json`, бинарь фриза B) + +| Глава | `units_total` | `chunks_total` | `expected_usd` | знаков | +|---|---|---|---|---| +| 1 `第一节:纵身亡魔心仍不悔` | 2 | 2 | 0.032424 | 3287 | +| 2 `第二节:逆光阴五百年觉悟` | 1 | 2 | 0.028459 | 3051 | +| 3 `第三节:请一边玩蛋去` | 1 | 2 | 0.023237 | 2421 | +| **книга** | **4** | **6** | `expected_usd 2.08412008` · `book_once_usd 2.00` · `step_max_usd 0.13647876` | 8759 | + +Смета ТЕКСТА — сумма глав, **$0.084120**; `book_once_usd` $2.00 это ПОТОЛКИ контура банка +(`gates.terminology.budget_usd 1.00` + `classify_budget_usd 1.00`), а не смета. +⭐ **Все семь чисел этой таблицы совпали с манифестом прогона A до последнего знака** — при другом +бинаре, другом каталоге и другом `book_id`. Это сильная улика тождества покупаемого. + +## 2. ⛔ МЕРА ДЕНЕЖНОЙ ДЫРЫ — пре-регистрируется ЗДЕСЬ, до первого цента + +**Прибор ПРОВЕРЕН ДО ПРИМЕНЕНИЯ:** те же запросы, прогнанные по сохранённой базе прогона A, дают +его числа. Это не «похоже», а пере-снято: 33 строки · 33 уникальных `id` · 2 трейса · $0.419423. + +* **единица** = `(chapter, chunk_idx, stage, role)`. Номера попытки в ключе НЕТ намеренно: единица + ОХВАТЫВАЕТ попытки. ⚠ У стадии `terminology` все строки несут `chapter = 0`, то есть `chunk_idx` + там — индекс майнинг-батча, а не чанк главы. Одно слово покрывает ДВА предмета; в каждом числе + называется, какой именно. +* **знаменатель** = единицы, за которые заплачено хоть раз (`sum(cost_usd) > 0`). **Печатаются ОБА + счёта.** На A фильтр не отсеял ничего (17 и 17) ⇒ его поведение не проверялось ни разу. +* **ЧИСЛИТЕЛЬ ПЕРВЫЙ — выброшенная покупка:** единицы, где есть хоть одна оплаченная и НЕ принятая + попытка (`ok = 0 AND cost_usd > 0`). **База A: 4 из 17 = 23.5 % ЕДИНИЦ**, тогда как по ВЫЗОВАМ то + же событие даёт 4 из 27 = **14.8 %**. Две меры расходятся в полтора раза — потому мера и объявлена. +* **ЧИСЛИТЕЛЬ ВТОРОЙ — ПЕРЕ-МАЙНИНГ БАНКА, и он НЕ называется «пере-оплатой».** Единицы, за + ПРИНЯТЫЙ результат которых заплачено больше одного раза (`ok = 1`). На A это вся стадия + терминологии. **Главное число — «все платежи КРОМЕ ПЕРВОГО» = $0.028742 (6.9 %), нижняя граница; + рядом печатается «все КРОМЕ ПОСЛЕДНЕГО» = $0.035723 (8.5 %).** Разница 24 %, и обе ориентации + получены ПОРЯДКОМ — тем самым методом, на котором прошлая смена получила ложные 54.9 %. Поля, + называющего вызов-источник, у строк банка НЕТ (`glossary` 21 колонка, вызова среди них нет; + `glossary_revisions` 0 строк), поэтому ориентация выбирается явно и объявляется. + ⛔ **И почему это НЕ «деньги за пустоту»** (замерено мной на артефактах A ДО прогона B): второй + заход терминологии ИЗМЕНИЛ передачу у **12 термов из 69**, и **11 из 12 уехали читателю именно в + ФИНАЛЬНОЙ форме** (стоп-форма — 0 из 12; один терм не встретился ни в какой). Это ЦЕНА + ПЕРЕ-МАЙНИНГА БАНКА ПОСЛЕ ЧЕЛОВЕЧЕСКОЙ ПОДПИСИ, а не выброшенная покупка. +* **ДВА ПУТИ К ДЕНЕЖНОМУ ЧИСЛУ.** Путь 1 — по колонке `degraded`; путь 2 — по `ok = 0 AND cost_usd > 0`, + колонки `degraded` не касаясь. На A оба дали **$0.106472 = 25.4 %**, но совпадают они ПО + ПОСТРОЕНИЮ. ⇒ совпадение на B — слабая улика; **РАСХОЖДЕНИЕ — сильная находка**: классификатор и + признак приёмки разъехались. ⚠ **25.4 % — доля ДЕНЕГ, 23.5 % — доля ЕДИНИЦ**, подменять нельзя. + +## 3. ⛔ ДВЕ СЛЕПЫЕ ПОВЕРХНОСТИ ПРИБОРА КОНСИСТЕНТНОСТИ, НАЗВАННЫЕ ДО ПРОГОНА + +Обе найдены на артефактах прогона A, обе меняют то, как прибор B строится. + +**(а) ГРАНИЦА БАНКА.** Прибор A сверял отгруженный текст со СТОП-ПРЕДЛОЖЕНИЕМ банка +(`project.db.bank.json`, снят в момент подписи), а читатель получил текст, сделанный под ФИНАЛЬНЫМ +банком: терминология пере-майнится ПОСЛЕ подписи, и редакторская волна идёт после неё +(по `request_log` прогона A: терминология-2 кончается 01:42:10, редактор идёт 01:43:54 → 01:54:44). +⇒ **прибор B читает ФИНАЛЬНЫЙ банк и ПЕЧАТАЕТ, какую версию он прочёл.** Класс — `D39.228` п.5. + +**(б) ЗНАМЕНАТЕЛЬ САМОГО ПРИБОРА.** На A он нашёл передачу у **40 термов из 69**, а у **29** вернул +пусто — и записал их в «консистентные». Его итог «отдано более чем одной формой: 0» посчитан по 40, +а читается как утверждение о книге. ⇒ **прибор B печатает всегда: термов всего N · передачу нашёл у +M · пусто у K, и K проверяется ТЕКСТОМ**, а не принимается за консистентность. + +## 4. Пре-рег-таблица: узел → что ДОЛЖНО произойти → чем предъявлено → что считается провалом + +| # | Узел | Ожидание (числом или строкой) | Чем предъявлено | Провал | +|---|---|---|---|---| +| 1 | Стенд — мой | слушатель `127.0.0.1:8098` принадлежит МОЕМУ pid; на `11434` в платной фазе НЕ слушает НИКТО | `ss -ltnp` + pid-файл | чужой процесс на порту ⇒ СТОП | +| 2 | Код — из фриза | у ПЯТИ бинарей `vcs.revision=baa06ce…`, `modified=false`, строк `vcs.*` > 0 | `go version -m` | любое из трёх не так ⇒ СТОП | +| 3 | Гость и деньги | ОТДЕЛЬНАЯ платная учётка; грант $2.50 одной строкой `credit_ledger.kind='grant'` | `tmplatformctl balance` + строка леджера, ФИЛЬТРОВАННАЯ по учётке | дымовые деньги попали в счёт ⇒ число недействительно | +| 4 | Интейк | `201`, `chapter_count = 3`; ⚠ `character_count` ≈ 9677 (счёт интейка по потоку, ПРИБЛИЗИТЕЛЬНЫЙ), точный — `source_chars = 8759` | тело 201 + строка `books` | не 3 главы ⇒ СТОП до оплаты | +| 5 | Срез совпал | `units_total = 4`, по главам **2/1/1**, `chunks_total = 6` | `chapters`+`units` в Postgres против §1.2 | расхождение ⇒ СТОП до оплаты | +| 6 | Цена и бонд | `term_consistency_funded: true`, `verdict: covers_all`, `affordable_chapters: 3`, холд **2 241 631 µUSD** = ⌈84 121 × 1.25⌉ + 136 479 + 2 000 000 | `GET /v0/books/{id}/run-options`, тело в файле | `false` ⇒ СТОП до оплаты | +| 7 | Старт | `202`, `status: translating`, `ordered_chapters: 3`, `runs.bond_funded = t`. ⚠ **Ожидается, что ответ старта скажет `term_consistency_funded: false`** при уже выставленном `bond_funded = t` — находка 7 прогона A; ВОСПРОИЗВЕДЕНА мной на $0-руке | ответ + строка `runs` | отказ старта ⇒ СТОП и пинг | +| 8 | Ключи живы | первый платный вызов не отказан по авторизации | `request_log.err` на первой строке | отказ провайдера ⇒ СТОП и пинг (не наша поломка) | +| 9 | Кадры | `events.jsonl` несёт `hello · progress · unit_done · spend · bank_stop · finished`; `run_attempts.last_seq` растёт **ПО ПОПЫТКАМ** | счёт кадров по типам, per-attempt | кадров нет при живом юните ⇒ дефект | +| 10 | Банк-стоп | `runs.status = awaiting_bank`, exit **3**, `.bank.json` несёт непустой `proposed[]` и `terms: []` | статус + exit + КОПИЯ сайдкара (резюм её уничтожает) | стоп не наступил ⇒ исход «узел не наступил», не провал | +| 11 | Проекция банка | `bank_terms` = **0** ПОКА прогон не закрыт и **> 0** ПОСЛЕ | `GET …/bank` дважды + `select count(*)` | ноль ПОСЛЕ закрытия ⇒ дефект | +| 12 | Дверь правок | `preview:true` → `changed:true`, на диске НИЧЕГО; `preview:false` → `state: applied` + `mined-delta.yaml` | тела обоих ответов В ФАЙЛАХ + sha файла до/после | преview написал на диск ⇒ дефект | +| 13 | Резюм (**РОВНО ОДИН**) | `202`, вторая строка `run_attempts`, `exit_code = 0` | `run_attempts` | не возобновился ⇒ дефект | +| 14 | Книга переведена | `units.state = translated` у всех **4** юнитов | `select state, count(*)` + контрольный счёт юнитов книги | любой не `translated` ⇒ книга не доведена | +| 15 | Сборка | `complete: true`, `pending/withheld/incomplete/stale/ghost = 0`, `stale_unknown: false`, `config_drift: false` | stdout `tmctl build` (на API этих полей НЕТ) | любое не так ⇒ книга не целая | +| 16 | Выгрузка | `exports.state = ready`, `exports.complete = t`, `size_bytes` = числу скачанных байт, ОБА формата | строка `exports` + длина скачанного | `complete` не `t` ⇒ дверь отдала книгу с дырой | +| 17 | ФАЙЛ | байты скачаны, sha256 напечатан; EPUB читается структурно (zip + `container.xml` + `nav` + spine = 3) | sha256 + разбор zip | не открывается ⇒ провал | +| 18 | Человек прочитал | первый абзац — связный русский текст | цитата в отчёте | не текст ⇒ провал | +| 19 | Деньги сошлись | Σ `request_log.cost_usd` ≈ кадр `spend` ≈ Σ `settlement` ЭТОЙ учётки ≈ (грант − баланс); открытых резерваций **0** | четыре счёта в µUSD рядом, ВСЕ фильтрованы по учётке | расхождение — НАЗВАТЬ, не подгонять | +| 20 | Ничего не реплеено | `tm_hit = 1` печатается С РАЗБИВКОЙ ПО ПОПЫТКАМ **по `trace_id`** (он несёт номер попытки), `distinct model_actual` не содержит `local-*` | запросы к `project.db` | есть `local-*` ⇒ прогон измерил заглушку | +| 21 | **v17 `wave_selection`** | **10 строк = 6 `draft` + 4 `edit`**, и волны стоят над РАЗНЫМИ снапшотами (черновик — базовый банк, редактор — обогащённый) | `select wave, count(*)` + `snapshot_id` + `injected_srcs` | строк нет ⇒ **пинг, а не ноль в отчёте**: писатель гейтится `r.memory != nil` | +| 22 | **v18 `reasoning_in_completion`** | **ТРЁХЗНАЧНО**: сколько `NULL`, сколько `0`, сколько `>0`, сумма = числу строк. Ожидание: на DeepSeek `>0` преобладает | три счёта + контроль суммы | «не ноль» вместо трёхзначности ⇒ вопрос не задан | +| 23 | Консистентность | `I1`/`I2` С ПЕЧАТАЕМЫМИ знаменателями + «термов N · передачу нашёл у M · пусто у K» + **какую версию банка прочёл прибор** | `QualityReport().Consistency` + свой счёт по отгруженному тексту | число без знаменателя в этом паке не принимается | +| 24 | ⭐ Вычитка | опус-читатель получает ВЕСЬ перевод и ВЕСЬ оригинал; печатает знаков оригинала (**контроль: 9021**), знаков перевода, ПОСЛЕДНЕЕ предложение обоих | его отчёт | не напечатал доказательство дочитывания ⇒ отчёт не принимается | +| 25 | ⭐ Сверка приборов | таблица 2×2 заполнена ИМЕНАМИ находок; левая нижняя клетка («прибор смолчал, читатель нашёл») названа поимённо | §4.6 отчёта | клетка пуста без довода ⇒ сверка не сделана | + +## 5. Стоп-правила (остановка и пинг, не решение сессии) + +1. фактическая трата дошла до **$1.00** (1 000 000 µUSD); +2. `runs.paused_reason` НЕПУСТО — любое из четырёх (`run_limit_reached · credit_exhausted · daily_ceiling · ceiling_unknown`); +3. срез не совпал с §1.2 — стоп ДО оплаты; +4. `term_consistency_funded: false` в `run-options` — стоп ДО оплаты; +5. бинарь не из фриза — стоп до устранения; +6. что-либо слушает `127.0.0.1:11434` в платной фазе — стоп: прогон измерял бы заглушку. + +## 6. ⛔ ОГРАНИЧЕНИЯ, ОБЪЯВЛЕННЫЕ ДО ПРОГОНА + +1. **ЭСКАЛАЦИЯ В B НЕ ИСПОЛНЯЕТСЯ.** `pipeline-c1.yaml:223` несёт `escalation.budget_usd: 0`, а + комментарий строкой `:221` гласит: «Приёмочная сессия 蛊真人 ОБЯЗАНА выставить budget_usd>0». + Решение оркестратора: **не трогать**, потому что тождество покупающего файла и ценовой модели + (§1.1) стоит дороже одного непроверенного пути. ⇒ B, как и A, о работе эскалации не говорит + НИЧЕГО. Сам этот факт идёт находкой вне заказа: механизм построен, ратифицирован и не может + исполниться ни на одном боевом конфиге без ручного оверрайда, которого никто ни разу не сделал. +2. **Судьи на пути нет** (`role: judge` живёт только в `c2`), **гейт `coverage` выключен** ⇒ из пяти + гейтов работают четыре. Проверяемый путь: черновик → редактор → четыре $0-гейта → выгрузка. +3. **`epubcheck` не гоняется** — `java` на хосте нет и поставить нельзя. EPUB судится структурно. +4. **ТРИ следа — не три свидетеля.** Postgres платформы МАТЕРИАЛИЗУЕТСЯ из `events.jsonl` движка, а + текст и файл платформа берёт теми же вербами `tmctl`. По-настоящему независимы только скачанные + байты и их sha256, плюс systemd-маркеры выхода. Биллинга провайдера у нас нет вовсе. +5. **Доллары — НАША модель поверх токенов провайдера.** Провайдер сообщает ТОКЕНЫ; цену умножаем мы, + по пину `models.yaml`. На 11.09 пин flash был ВЫШЕ вендорской страницы на 13 % (D39.247 п.3). +6. **Пик/офф-пик.** Прогон A шёл 01:29–01:54 UTC в пятницу — ВНУТРИ пикового окна DeepSeek + (01–04 и 06–10 UTC, пн–пт). B идёт вечером пятницы, то есть в ОФФ-ПИК. Леджер осознанно считает + по ПИКОВОЙ цене (`D39.136`: потолок обязан срабатывать раньше) ⇒ **записанная цена B будет вдвое + выше настоящего счёта, а записанная цена A — нет.** На сравнение ЛЕДЖЕРОВ это не влияет (оба + пиковые), на «сколько мы заплатили» — влияет вдвое. Называется здесь, чтобы не открылось потом. +7. **ПРЕТРЕЙН — процедура, а не запрет.** `蛊真人` узнаётся моделями 4 из 5. **Каждая находка о + качестве обязана нести ДВЕ цитаты рядом — фрагмент ОРИГИНАЛА и фрагмент ПЕРЕВОДА.** Находка, + которая не может показать место в исходнике, — не находка, а воспоминание. + +## 7. Инструменты (замораживаются вместе с этим пре-регом) + +* `eval/cold_run_b/door.py` — драйвер: гейт · вход · грант · интейк · опции · старт · вахта · улика · + подпись · резюм · выгрузка · произвольная проба. **Каждый запрос И каждый ответ ложатся в + `evidence/http/` ДО того, как вернутся вызывающему** — дефект харнесса A, из-за которого пять + строк его пре-рег-таблицы остались без носителя (§10.12 его отчёта). +* `eval/cold_run_b/measure.py` — прибор денег и единиц §2. **Пере-снимает числа прогона A на его же + базе** как самопроверку перед применением к B. +* Из фриза A переиспользуются без правки: `eval/door_to_file/stub` ($0-заглушка провайдера), + `eval/door_to_file/zeropipe` (рендер $0-пайплайна). Оба собраны из клона B и несут его штамп. + +## 8. Что уже исполнено ДО фриза и чем предъявлено ($0) + +* **`GET /v0/capabilities` → `export_formats: ["txt","epub"]`** (непусто ⇒ все три гейта выгрузки + выполнены), `contract_version 0.15.0`, `bank_corrections_enabled: true`. +* **Пин фазы 0 — ЗАГРУЗЧИК, а не скан.** Движок ЗАГРУЗИЛ рендер $0-пайплайна бесплатным вербом + (`tmctl manifest --json`, EXIT=0). Контрольная величина рядом: достижимых ПЛАТНЫХ моделей в + `pipeline-c1.yaml` — **8**, в рендере — **0**, счёт по РАЗОБРАННОМУ YAML, а не по тексту. +* **$0-ДЫМ ПРОЙДЕН ЦЕЛИКОМ на ОТДЕЛЬНОЙ книге** (синтетический zh-текст, 1206 байт, 4 главы, + sha256 `10637234312895da4665ed0dff84f25dfacdffd842871425b1bba1cc612b80da`) и на ОТДЕЛЬНОЙ учётке: + стенд · гость · грант · интейк 201 · опции (4 главы, 4 юнита, бонд профинансирован) · старт 202 · + кадры · банк-стоп (exit 3) · дверь правок (preview НИЧЕГО не пишет — проверено sha файла до и + после) · резюм (exit 0) · выгрузка ОБОИХ форматов, `complete = t`, файлы скачаны. +* ⭐ **ОБЕ МИГРАЦИИ УЖЕ ИСПОЛНИЛИСЬ ЖИВЬЁМ, и это честно называется здесь, а не приписывается B.** + На дымовой базе: `wave_selection` — **8 строк = 4 `draft` + 4 `edit`**, волны стоят над РАЗНЫМИ + снапшотами (`6dabf7e5…` без инъекции против `76670592…` с тремя термами) ⇒ **ось, которой у + `retrieval_state` нет, работает.** `reasoning_in_completion` — `NULL` на всех 16 строках, и это + ВЕРНЫЙ трёхзначный ответ: локальная заглушка поля не сообщает. ⇒ **B покупает не «исполнилась ли + миграция», а её поведение НА ПРОВАЙДЕРЕ, КОТОРЫЙ ЭТО ПОЛЕ ШЛЁТ.** +* ⭐ **МИГРАЦИЯ ПРОВЕРЕНА НА ЗАПОЛНЕННОЙ БАЗЕ, а не только на пустой.** Копия базы прогона A + (v16, 33 строки `request_log`, 69 строк `glossary`) открыта бинарём B: `v16 -> v18`, EXIT=0, строки + целы (33 и 69), `wave_selection` создана пустой, **`reasoning_in_completion` = `NULL` на всех 33 + ранее написанных строках** — ровно то, что миграция обещает. Оригинал не тронут (sha сверена + до и после). Пустая база про данные не говорит ничего; эта — говорит. diff --git a/eval/cold_run_b/README.md b/eval/cold_run_b/README.md new file mode 100644 index 00000000..06e61242 --- /dev/null +++ b/eval/cold_run_b/README.md @@ -0,0 +1,26 @@ +# `cold_run_b` — харнесс холодного прогона B + +Инструменты пака «ХОЛОДНЫЙ ПРОГОН B» (`docs/COLD_RUN_B_SESSION_PROMPT.md`). Пре-регистрация, +таблица ожидаемых исходов и результаты — `docs/experiments/25-door-to-file-b.md`. + +| Файл | Что делает | Чем проверен | +|---|---|---| +| `door.py` | драйвер: гейт · вход · грант · интейк · опции · старт · вахта · улика · подпись · резюм · выгрузка · произвольная проба с ЗАРАНЕЕ записанным ожиданием. **Отказывает**, а не предупреждает | $0-дым прошёл им целиком: интейк → банк-стоп → подпись → резюм → обе выгрузки → файл | +| `measure.py` | прибор денег и ЕДИНИЦ: знаменатель, оба числителя, обе ориентации второго, два пути к денежному числу | `--selfcheck` пере-снимает КАЖДОЕ опубликованное число прогона A на его же базе и отказывает, если хоть одно сдвинулось | + +## Три вещи, ради которых это не копия харнесса прогона A + +1. **Каждый запрос и каждый ответ ложатся в файл ДО того, как вернутся вызывающему.** Драйвер A + печатал тела на экран и не писал их никуда — пять строк его пре-рег-таблицы остались без + носителя, и целая находка состояла только из тела ответа, которого не сохранилось. +2. **Живость читается ПО ПОПЫТКАМ.** `max(last_seq)` по всем попыткам во время второй выглядит + замершим на значении первой. Номер попытки не вычисляется по времени — он НАЗВАН в `trace_id`. +3. **Деньги читаются фильтром по ОДНОЙ учётке.** Сборщик A суммировал леджер по всем учёткам стенда + и подал два гранта как один; дымовые деньги и боевые не должны встречаться ни в одном числе. + +## Что переиспользуется из фриза прогона A без единой правки + +`eval/door_to_file/stub` ($0-заглушка провайдера `kind: local` на `127.0.0.1:11434`) и +`eval/door_to_file/zeropipe` (рендер боевого пайплайна на $0-пару, печатает счёт достижимых ПЛАТНЫХ +моделей в обоих файлах). Оба собраны из клона B и несут его VCS-штамп — девиация §12.5 отчёта A +(«два моих инструмента собраны не из фриза») здесь закрыта построением. diff --git a/eval/cold_run_b/door.py b/eval/cold_run_b/door.py new file mode 100755 index 00000000..b298f9ef --- /dev/null +++ b/eval/cold_run_b/door.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 +"""door.py — the driver of cold run B: one book from the intake door to a downloaded file. + +Why this exists next to `eval/door_to_file/drive.sh` rather than as an edit of it: that harness is +frozen evidence of run A, and run A named two defects in it that this run cannot inherit. + + * IT PRINTED EVERY HTTP ANSWER TO THE SCREEN AND WROTE NONE TO DISK. Five of A's pre-registration + rows ended with no carrier at all (§10.12 of its report): the `run-options` body, the 202 of the + start, the corrections door's `changed`/`applied`, and a whole finding that consisted of nothing + but a response body. Here EVERY request and EVERY answer is appended to evidence/http/ before + the caller sees it, so an assertion about a body can be re-read rather than remembered. + * ITS LIVENESS WATCH READ max(last_seq) ACROSS ATTEMPTS, so during the second attempt the number + looked frozen at the first attempt's value while work was going on. Here every liveness number + is read PER ATTEMPT, and the attempt is taken from the row, never inferred from time. + +Two rules this file keeps because breaking them is what makes a run measure nothing: + * money is read filtered by ONE account. A's collector summed the ledger over every account on the + stand and reported two grants as one (§14.2); the smoke's account and the paid account must never + meet in a number. + * a claim about a count prints its control quantity beside it. "0 rows" and "no such table" look + the same in output, and in money that is the difference between "did not spend" and "did not look". +""" +import argparse, http.cookiejar, json, mimetypes, os, re, subprocess, sys, time, urllib.error, urllib.request, uuid + +W = os.environ.get("W", "/home/ubuntu-26/tm-coldrun-b") +ADDR = os.environ.get("ADDR", "127.0.0.1:8098") +DSN = os.environ.get("TM_PLATFORM_DSN", "postgres://postgres@/tm_coldrun_b?host=/tmp&port=55433&sslmode=disable") +PSQL = os.path.expanduser("~/.local/pgsql/bin/psql") +HTTPDIR = os.path.join(W, "evidence", "http") +STATE = os.path.join(W, "stand", "run-state.json") + +# An opener that does NOT go through the environment's proxy: this host exports a webshare proxy and +# a WinINET-style NO_PROXY that Go, curl and urllib all fail to parse, so a request to 127.0.0.1 goes +# out to the proxy and comes back 403 (provider-quirks, "Прокси на localhost"). +# The jar is a FILE: each step is its own process, and a jar that lives only in memory logs in again +# on every call — which looks like a working session right up to the moment a step needs the one it +# thought it had. +JARFILE = os.environ.get("TMB_JAR", os.path.join(W, "stand", "cookies.txt")) +JAR = http.cookiejar.MozillaCookieJar(JARFILE) +try: + JAR.load(ignore_discard=True, ignore_expires=True) +except Exception: + pass +OPENER = urllib.request.build_opener( + urllib.request.ProxyHandler({}), urllib.request.HTTPCookieProcessor(JAR)) + + +def jar_save(): + os.makedirs(os.path.dirname(JARFILE), exist_ok=True) + JAR.save(ignore_discard=True, ignore_expires=True) + +_seq = [0] + + +def state_load(): + try: + with open(STATE) as f: + return json.load(f) + except FileNotFoundError: + return {} + + +def state_put(**kw): + s = state_load() + s.update(kw) + os.makedirs(os.path.dirname(STATE), exist_ok=True) + with open(STATE, "w") as f: + json.dump(s, f, indent=2) + return s + + +def sql(q): + out = subprocess.run([PSQL, DSN, "-At", "-F", "\t", "-c", q], + capture_output=True, text=True) + if out.returncode != 0: + raise SystemExit("psql refused: " + out.stderr.strip()) + return [l.split("\t") for l in out.stdout.splitlines()] + + +def sql1(q, default=None): + rows = sql(q) + return rows[0][0] if rows and rows[0] and rows[0][0] != "" else default + + +def api(method, path, body=None, ctype=None, label=None, raw=False, headers=None): + """One request. The request AND the answer are on disk before this returns.""" + _seq[0] += 1 + n = _seq[0] + url = f"http://{ADDR}{path}" + req = urllib.request.Request(url, data=body, method=method) + req.add_header("X-TM-Client", "coldrun-b-driver") + if ctype: + req.add_header("Content-Type", ctype) + for k, v in (headers or {}).items(): + req.add_header(k, v) + t0 = time.time() + try: + r = OPENER.open(req, timeout=300) + code, data, hdrs = r.status, r.read(), dict(r.headers) + except urllib.error.HTTPError as e: + code, data, hdrs = e.code, e.read(), dict(e.headers) + except Exception as e: # connection refused, timeout, … + code, data, hdrs = 0, str(e).encode(), {} + ms = int((time.time() - t0) * 1000) + name = f"{n:03d}-{(label or path.strip('/').replace('/', '_'))[:60]}" + rec = {"n": n, "at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "method": method, "url": url, + "status": code, "ms": ms, "headers": hdrs, + "request_ctype": ctype, "request_bytes": len(body or b"")} + os.makedirs(HTTPDIR, exist_ok=True) + if raw: + with open(os.path.join(HTTPDIR, name + ".bin"), "wb") as f: + f.write(data) + rec["body_file"] = name + ".bin" + rec["body_bytes"] = len(data) + else: + try: + rec["body"] = json.loads(data.decode("utf-8")) + except Exception: + rec["body_text"] = data.decode("utf-8", "replace")[:20000] + with open(os.path.join(HTTPDIR, name + ".json"), "w") as f: + json.dump(rec, f, ensure_ascii=False, indent=2) + with open(os.path.join(W, "evidence", "http-index.jsonl"), "a") as f: + f.write(json.dumps({k: rec[k] for k in ("n", "at", "method", "url", "status", "ms")}) + "\n") + jar_save() + return code, (rec.get("body") if not raw else data), hdrs + + +def multipart(fields, filefield, filepath): + b = "----tmb" + uuid.uuid4().hex + out = [] + for k, v in fields.items(): + out.append(f"--{b}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n".encode()) + fn = os.path.basename(filepath) + ct = mimetypes.guess_type(fn)[0] or "application/octet-stream" + out.append(f"--{b}\r\nContent-Disposition: form-data; name=\"{filefield}\"; filename=\"{fn}\"\r\n" + f"Content-Type: {ct}\r\n\r\n".encode()) + out.append(open(filepath, "rb").read()) + out.append(f"\r\n--{b}--\r\n".encode()) + return b"".join(out), f"multipart/form-data; boundary={b}" + + +# ---------------------------------------------------------------- the gate + +FREEZE_SHA = os.environ.get("FREEZE_SHA", "baa06cef7b2b30b8f7cbe031f3d0fabc03a01a55") +BINARIES = ["tmctl", "tmplatformd", "tmplatformctl", "stub", "zeropipe"] + + +def stamp(path): + out = subprocess.run(["go", "version", "-m", path], capture_output=True, text=True).stdout + vcs = [l for l in out.splitlines() if "vcs." in l] + rev = next((l.split("vcs.revision=")[1].strip() for l in vcs if "vcs.revision=" in l), None) + mod = next((l.split("vcs.modified=")[1].strip() for l in vcs if "vcs.modified=" in l), None) + return rev, mod, len(vcs), len(out.splitlines()) + + +def step_gate(args): + """Refuses; never warns. Three conditions on the stamp, not one — a binary built in a linked + worktree carries NO vcs.* lines, and a gate that only forbids modified=true passes it always.""" + bad = [] + print("=== binaries: the frozen revision, a clean tree, and a stamp that EXISTS ===") + for b in BINARIES: + p = os.path.join(W, "bin", b) + rev, mod, n, total = stamp(p) + print(f" {b:<14} revision={(rev or 'none')[:12]} modified={mod} vcs_lines={n} (control: build lines {total})") + if n == 0: + bad.append(f"{b}: no vcs.* lines — this gate would be vacuous") + if rev != FREEZE_SHA: + bad.append(f"{b}: revision is not the frozen one") + if mod != "false": + bad.append(f"{b}: built from a dirty tree") + print("=== the stand is MINE: a pid on the port, not a 200 from someone else's process ===") + listeners = subprocess.run(["ss", "-ltnp"], capture_output=True, text=True).stdout.splitlines()[1:] + def owner(port): + for l in listeners: + if f":{port} " in l: + return l.split("users:")[-1] if "users:" in l else l + return None + for port, must in ((ADDR.split(":")[1], True),): + o = owner(port) + print(f" port {port}: {o or 'NOTHING LISTENS'}") + if must and not o: + bad.append(f"port {port}: there is no stand") + stub = owner("11434") + phase = args.phase + if phase == "smoke": + print(f" port 11434 (the $0 stub, REQUIRED in this phase): {stub or 'NOTHING LISTENS'}") + if not stub: + bad.append("the smoke has no stub to answer the local provider") + else: + if stub: + bad.append("something LISTENS on the local provider's address during a PAID phase: a " + "configuration that still reached a local model would be served free prose " + "and the run would measure nothing while looking green") + print(f" port 11434: ⛔ {stub}") + else: + print(f" port 11434: nothing listens (control: {len(listeners)} listeners on this host) " + f"— a local model would now fail LOUDLY") + print("=== what will be bought: sha256 of every file that decides it ===") + for name, path, expect in args.sha or []: + if not os.path.exists(path): + print(f" {name}: ABSENT at {path}") + bad.append(f"{name} is absent") + continue + got = subprocess.run(["sha256sum", path], capture_output=True, text=True).stdout.split()[0] + ok = (expect == "" or expect == got) + print(f" {name}: {got}{'' if expect == '' else (' (matches the pre-registration)' if ok else ' ⛔ the pre-registration says ' + expect)}") + if not ok: + bad.append(f"{name}: sha does not match the pre-registration") + print("=== the freeze tree itself ===") + head = subprocess.run(["git", "-C", os.path.join(W, "freeze"), "rev-parse", "HEAD"], + capture_output=True, text=True).stdout.strip() + dirty = subprocess.run(["git", "-C", os.path.join(W, "freeze"), "status", "--porcelain"], + capture_output=True, text=True).stdout.splitlines() + print(f" HEAD {head} dirty lines: {len(dirty)}") + if os.path.exists("/tmp/.git"): + bad.append("/tmp/.git exists: a build here would lose its stamp") + if bad: + for b in bad: + print(" ⛔ " + b) + raise SystemExit("THE GATE REFUSED; nothing was bought") + print("GATE PASSED") + + +# ---------------------------------------------------------------- the steps + +def step_capabilities(args): + code, body, _ = api("GET", "/v0/capabilities", label="capabilities") + print(f"HTTP {code}") + print(json.dumps(body, ensure_ascii=False, indent=2)[:2000]) + if code != 200: + # "empty list" and "never answered" are the same shape in output, and this is the pin the + # whole export third of the path hangs on: say which one happened. + print(f"⛔ the capabilities probe did not answer 200 — export_formats is UNKNOWN, not empty") + return + fmts = (body or {}).get("export_formats") + print(f"export_formats = {fmts!r} — {'NON-EMPTY: all three export gates are satisfied' if fmts else '⛔ EMPTY: the export doors are not mounted and the last third of the path does not exist'}") + + +def step_login(args): + code, body, _ = api("POST", "/auth/dev-login", label="dev-login") + print(f"dev-login HTTP {code}") + rows = sql("select id, coalesce(email,'(none)') from users order by created_at desc limit 5") + print(f" users on this stand: {len(sql('select id from users'))} (control) — newest: {rows[0] if rows else 'none'}") + if rows: + state_put(user=rows[0][0]) + print(f" remembered user={rows[0][0]}") + + +def step_grant(args): + out = subprocess.run([os.path.join(W, "bin", "tmplatformctl"), "grant", + "--user", args.user, "--usd", args.usd, "--note", args.note], + capture_output=True, text=True) + print(out.stdout.strip() or out.stderr.strip()) + bal = subprocess.run([os.path.join(W, "bin", "tmplatformctl"), "balance", "--user", args.user], + capture_output=True, text=True) + print(bal.stdout.strip() or bal.stderr.strip()) + rows = sql(f"select kind, amount_micro_usd from credit_ledger where user_id='{args.user}' order by id") + total = sum(int(r[1]) for r in rows) + print(f" ledger rows for THIS account: {len(rows)} (control: rows for ALL accounts " + f"{len(sql('select id from credit_ledger'))}), sum {total} µUSD") + + +def step_intake(args): + body, ct = multipart({"title": args.title, "source_lang": "zh", "target_lang": "ru"}, "file", args.path) + code, b, _ = api("POST", "/v0/books", body=body, ctype=ct, label="intake") + print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}") + if code != 201: + raise SystemExit("the intake refused") + state_put(book=b["id"]) + sha = subprocess.run(["sha256sum", args.path], capture_output=True, text=True).stdout.split()[0] + print(f" remembered book={b['id']} source sha256={sha}") + + +def _await_options(book, tries=20): + for _ in range(tries): + code, b, _ = api("GET", f"/v0/books/{book}/run-options", label="run-options") + if code == 200 and isinstance(b, dict) and "order" in b: + return code, b + time.sleep(3) + return code, b + + +def step_options(args): + book = state_load()["book"] + code, b = _await_options(book) + print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}") + order = (b or {}).get("order", {}) + ch = order.get("chapters_left") + funded = order.get("term_consistency_funded") + units = sql1(f"select count(*) from units u join chapters c on c.id=u.chapter_id where c.book_id='{book}'", "0") + per = sql(f"""select c.number, count(u.id) from chapters c left join units u on u.chapter_id=c.id + where c.book_id='{book}' group by 1 order by 1""") + for n, k in per: + print(f" chapter {n}: units {k}") + print(f" units_total = {units} chapters_left = {ch} term_consistency_funded = {funded}") + bad = [] + if args.chapters is not None and str(ch) != str(args.chapters): + bad.append(f"the slice is not the pre-registered one: {ch} chapters, expected {args.chapters}") + if args.units is not None and str(units) != str(args.units): + bad.append(f"units_total is {units}, the pre-registration says {args.units}") + if args.require_funded and funded is not True: + bad.append(f"term_consistency_funded is {funded}: the hold does not carry the book bond and " + f"the run would quietly lose its terminology consolidation") + if bad: + for x in bad: + print(" ⛔ " + x) + raise SystemExit("STOP before paying") + print("OPTIONS ACCEPTED") + + +def step_start(args): + book = state_load()["book"] + payload = {"stop_for_signing": bool(args.stop_for_signing)} + if args.chapters is not None: + payload["chapters"] = args.chapters + if args.characters is not None: + payload["characters"] = args.characters + hdrs = {"Idempotency-Key": args.idempotency_key} if args.idempotency_key else {} + code, b, _ = api("POST", f"/v0/books/{book}/runs", body=json.dumps(payload).encode(), + ctype="application/json", label="start", headers=hdrs) + print(f"HTTP {code}\nrequest: {json.dumps(payload, ensure_ascii=False)}\n{json.dumps(b, ensure_ascii=False, indent=2)}") + if code in (200, 201, 202) and isinstance(b, dict) and b.get("id"): + state_put(run=b["id"]) + print(f" remembered run={b['id']}") + return code, b + + +def _engine_numbers(book): + """What the ENGINE's own project database says. Returns (rows, calls, free_replays, µUSD).""" + d = sql1(f"select workdir from books where id='{book}'") + db = os.path.join(d, "project.db") if d else None + if not db or not os.path.exists(db): + return None, db + import sqlite3 + c = sqlite3.connect(f"file:{db}?mode=ro", uri=True) + rows = c.execute("select count(*) from request_log").fetchone()[0] + hits = c.execute("select count(*) from request_log where tm_hit=1").fetchone()[0] + cost = c.execute("select coalesce(sum(cost_usd),0) from request_log").fetchone()[0] + per = c.execute("""select substr(trace_id,-1), count(*), sum(tm_hit=1), round(sum(cost_usd)*1e6) + from request_log group by 1 order by 1""").fetchall() + c.close() + return {"rows": rows, "calls": rows - hits, "free": hits, "micro": round(cost * 1e6), + "per_attempt": per}, db + + +def step_watch(args): + """Liveness is read ACTIVELY and on two axes, per ATTEMPT. A growing log is not a result, and a + hung job is indistinguishable from a running one by the completion signal alone.""" + s = state_load() + book, run = s.get("book"), s.get("run") + eng, db = _engine_numbers(book) + att = sql(f"select attempt_no, last_seq, coalesce(exit_code,-999), coalesce(spend_micro_usd,0), unit_name " + f"from run_attempts where run_id='{run}' order by attempt_no") + status = sql1(f"select status from runs where id='{run}'", "?") + paused = sql1(f"select coalesce(paused_reason,'') from runs where id='{run}'", "") + events = sql1(f"select count(*) from book_events where book_id='{book}'", "0") + user = s.get("user") + settled = sql1(f"select coalesce(sum(amount_micro_usd),0) from credit_ledger where user_id='{user}' and kind='settlement'", "0") + bal = sql1(f"select coalesce(sum(amount_micro_usd),0) from credit_ledger where user_id='{user}'", "0") + print(f"{time.strftime('%H:%M:%S')} status={status} paused={paused!r} book_events={events}") + for a in att: + unit = a[4] + active = subprocess.run(["systemctl", "--user", "show", unit + ".service", "-p", "ActiveState", "--value"], + capture_output=True, text=True).stdout.strip() or "unknown" + print(f" attempt {a[0]}: last_seq={a[1]} exit={a[2]} spend(cumulative)={a[3]}µUSD unit={active}") + if eng: + print(f" engine: request_log rows={eng['rows']} (calls={eng['calls']}, free replays={eng['free']}) " + f"spend={eng['micro']}µUSD | per attempt {eng['per_attempt']}") + else: + print(f" engine: no project database yet at {db}") + print(f" platform ledger for THIS account: settlements={settled}µUSD balance={bal}µUSD " + f"(control: ledger rows for ALL accounts {sql1('select count(*) from credit_ledger','0')})") + if paused: + raise SystemExit(f"paused_reason={paused} — STOP and ping the orchestrator") + if eng and args.stop_at and eng["micro"] >= args.stop_at: + raise SystemExit(f"spend reached {eng['micro']}µUSD ≥ {args.stop_at}µUSD — STOP and ping the orchestrator") + + +def step_evidence(args): + """Takes the copy the resume DESTROYS. exportBank is called again at the start of the next + attempt and overwrites .bank.json atomically; .bank-stop.txt is truncated before it is rewritten, + so it can only be read while the run stands still.""" + s = state_load() + book, run = s["book"], s["run"] + d = sql1(f"select workdir from books where id='{book}'") + out = os.path.join(W, "evidence", args.label) + os.makedirs(out, exist_ok=True) + status = sql1(f"select status from runs where id='{run}'", "?") + print(f"run status while the copy is taken: {status}") + if status == "translating": + raise SystemExit("the run is still moving: .bank-stop.txt would be read half-written") + n = 0 + for f in ("project.db.bank.json", "project.db.mined-signature.yaml", "project.db.bank-stop.txt", + "project.db.auto-bank.yaml", "project.db.manifest.json", "events.jsonl", + "project.db.mined-delta.yaml", "project.db"): + src = os.path.join(d, f) + if os.path.exists(src): + subprocess.run(["cp", src, out + "/"]) + sha = subprocess.run(["sha256sum", src], capture_output=True, text=True).stdout.split()[0] + print(f" {f}: {sha}") + n += 1 + print(f" copied {n} artefacts (control: the book directory holds {len(os.listdir(d))} entries)") + code, b, _ = api("GET", f"/v0/books/{book}/bank", label=f"bank-{args.label}") + t = (b or {}).get("terms") or [] + print(f" GET bank: HTTP {code}, terms={len(t)}, total={(b or {}).get('total')}, signed={(b or {}).get('signed')}") + rows = sql(f"select count(*), count(*) filter (where status='approved') from bank_terms where book_id='{book}'") + print(f" bank_terms in Postgres: all={rows[0][0]} approved={rows[0][1]}") + + +def step_sign(args): + book = state_load()["book"] + doc = open(args.doc, "rb").read() + code, b, _ = api("POST", f"/v0/books/{book}/bank/corrections", body=doc, + ctype="application/json", label=f"corrections-{args.label}") + print(f"HTTP {code}\nrequest: {doc.decode('utf-8')[:800]}\n{json.dumps(b, ensure_ascii=False, indent=2)[:2000]}") + return code, b + + +def step_resume(args): + run = state_load()["run"] + hdrs = {"Idempotency-Key": args.idempotency_key} if args.idempotency_key else {} + code, b, _ = api("POST", f"/v0/runs/{run}/resume", body=b"{}", ctype="application/json", + label=f"resume{args.label}", headers=hdrs) + print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}") + return code, b + + +def step_export(args): + book = state_load()["book"] + code, b, _ = api("POST", f"/v0/books/{book}/exports", body=json.dumps({"format": args.format}).encode(), + ctype="application/json", label=f"export-{args.format}-order") + print(f"order HTTP {code}: {json.dumps(b, ensure_ascii=False)}") + if code not in (200, 201, 202): + raise SystemExit("the export door refused") + eid = b["id"] + for _ in range(60): + time.sleep(2) + code, b, _ = api("GET", f"/v0/books/{book}/exports/{eid}", label=f"export-{args.format}-status") + st = (b or {}).get("state") + if st == "ready": + break + if st == "failed": + raise SystemExit(f"the export failed: {b}") + print(f"status HTTP {code}: {json.dumps(b, ensure_ascii=False)}") + code, data, _ = api("GET", f"/v0/books/{book}/exports/{eid}/content", + label=f"export-{args.format}-content", raw=True) + os.makedirs(os.path.join(W, "evidence", "file"), exist_ok=True) + p = os.path.join(W, "evidence", "file", f"book.{args.format}") + open(p, "wb").write(data) + sha = subprocess.run(["sha256sum", p], capture_output=True, text=True).stdout.split()[0] + print(f"download HTTP {code} bytes={len(data)} sha256={sha}") + row = sql(f"select id,format,state,complete,size_bytes from exports where id='{eid}'") + print(f" exports row (complete is read from Postgres — the wire deliberately does not carry it): {row}") + + +def step_probe(args): + """One ad-hoc request whose EXPECTED outcome was written down before it was sent (§4.8): a door's + answer read after the fact always looks right.""" + body = args.body.encode() if args.body else None + hdrs = {} + if args.idempotency_key: + hdrs["Idempotency-Key"] = args.idempotency_key + if args.header: + for h in args.header: + k, _, v = h.partition(":") + hdrs[k.strip()] = v.strip() + code, b, hh = api(args.method, args.path, body=body, + ctype=("application/json" if body else None), label=args.label, headers=hdrs) + verdict = "AS EXPECTED" if (args.expect_status is None or code == args.expect_status) else "⛔ NOT AS EXPECTED" + print(f"[{args.label}] {args.method} {args.path} -> HTTP {code} (expected {args.expect_status}) {verdict}") + txt = json.dumps(b, ensure_ascii=False)[:600] if not isinstance(b, bytes) else f"<{len(b)} bytes>" + print(f" body: {txt}") + if args.expect_contains and args.expect_contains not in txt: + print(f" ⛔ the answer does not contain {args.expect_contains!r}") + + +def main(): + ap = argparse.ArgumentParser() + sub = ap.add_subparsers(dest="cmd", required=True) + g = sub.add_parser("gate"); g.add_argument("--phase", default="paid"); g.set_defaults(f=step_gate, sha=[]) + for name, fn in (("capabilities", step_capabilities), ("login", step_login)): + p = sub.add_parser(name); p.set_defaults(f=fn) + p = sub.add_parser("grant"); p.add_argument("--user", required=True); p.add_argument("--usd", required=True) + p.add_argument("--note", default="cold run B"); p.set_defaults(f=step_grant) + p = sub.add_parser("intake"); p.add_argument("--path", required=True); p.add_argument("--title", required=True) + p.set_defaults(f=step_intake) + p = sub.add_parser("options"); p.add_argument("--chapters", type=int); p.add_argument("--units", type=int) + p.add_argument("--require-funded", action="store_true"); p.set_defaults(f=step_options) + p = sub.add_parser("start"); p.add_argument("--chapters", type=int); p.add_argument("--characters", type=int) + p.add_argument("--stop-for-signing", action="store_true"); p.add_argument("--idempotency-key") + p.set_defaults(f=step_start) + p = sub.add_parser("watch"); p.add_argument("--stop-at", type=int, default=1000000); p.set_defaults(f=step_watch) + p = sub.add_parser("evidence"); p.add_argument("--label", default="evidence"); p.set_defaults(f=step_evidence) + p = sub.add_parser("sign"); p.add_argument("--doc", required=True); p.add_argument("--label", default="sign") + p.set_defaults(f=step_sign) + p = sub.add_parser("resume"); p.add_argument("--label", default=""); p.add_argument("--idempotency-key") + p.set_defaults(f=step_resume) + p = sub.add_parser("export"); p.add_argument("--format", required=True); p.set_defaults(f=step_export) + p = sub.add_parser("probe"); p.add_argument("--method", default="GET"); p.add_argument("--path", required=True) + p.add_argument("--body"); p.add_argument("--label", required=True); p.add_argument("--expect-status", type=int) + p.add_argument("--expect-contains"); p.add_argument("--idempotency-key") + p.add_argument("--header", action="append"); p.set_defaults(f=step_probe) + a = ap.parse_args() + a.f(a) + + +if __name__ == "__main__": + main() diff --git a/eval/cold_run_b/measure.py b/eval/cold_run_b/measure.py new file mode 100644 index 00000000..f1c3d6d8 --- /dev/null +++ b/eval/cold_run_b/measure.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""measure.py — the money instrument of cold run B, pre-registered before the first cent. + +It answers ONE question the call-level numbers cannot: what share of a book's UNITS was paid for and +not kept. A unit is `(chapter, chunk_idx, stage, role)` and it SPANS attempts on purpose — the +attempt number is deliberately not in the key, because "we bought this cell twice" is a statement +about the cell, not about a retry. + +Why it is validated against run A before it is pointed at run B: an instrument that has never +reproduced a known number is indistinguishable from one that reads the wrong column. `--selfcheck` +re-derives run A's published figures from run A's own database and refuses if any of them moves. + +⚠ TWO THINGS THIS FILE REFUSES TO DO, both because a previous shift got a beautiful false number +from doing them: + * it never decides "which answer was accepted" by ORDER IN TIME. `ts` in this schema is second- + granular while `started_at` carries microseconds, so every checkpoint substitution is formally + "before" the attempt that made it. The attempt number is NAMED in `trace_id`; it is read, not + computed. + * it never calls the second numerator "over-payment". On run A the second terminology pass changed + the rendering of 12 terms out of 69 and 11 of those 12 are what the reader actually got. That is + the PRICE OF RE-MINING THE BANK AFTER A HUMAN SIGNATURE, not money bought for nothing. +""" +import argparse, collections, json, sqlite3, sys + + +def load(db): + c = sqlite3.connect(f"file:{db}?mode=ro", uri=True) + c.row_factory = sqlite3.Row + rows = [dict(r) for r in c.execute("select * from request_log order by id")] + c.close() + return rows + + +def attempt_of(row): + """The attempt is NAMED in the row. `trace_id` ends with it (tm-stream--).""" + t = row.get("trace_id") or "" + tail = t.rsplit("-", 1)[-1] + return tail if tail.isdigit() else "?" + + +def measure(rows): + total = sum(r["cost_usd"] for r in rows) + units = collections.defaultdict(list) + for r in rows: + units[(r["chapter"], r["chunk_idx"], r["stage"], r["role"])].append(r) + paid = {k: v for k, v in units.items() if sum(x["cost_usd"] for x in v) > 0} + + n1 = {k: v for k, v in units.items() if any(x["ok"] == 0 and x["cost_usd"] > 0 for x in v)} + n1_money = sum(x["cost_usd"] for v in n1.values() for x in v if x["ok"] == 0 and x["cost_usd"] > 0) + + n2 = {k: v for k, v in units.items() + if sum(1 for x in v if x["ok"] == 1 and x["cost_usd"] > 0) > 1} + # Both orientations are printed. Neither is derived from time: rows are ordered by the primary + # key, and which one is called "the extra" is a DECLARED choice, not a discovered fact. + but_first = 0.0 + but_last = 0.0 + for v in n2.values(): + acc = sorted([y for y in v if y["ok"] == 1 and y["cost_usd"] > 0], key=lambda z: z["id"]) + but_first += sum(x["cost_usd"] for x in acc[1:]) + but_last += sum(x["cost_usd"] for x in acc[:-1]) + + by_degraded = sum(r["cost_usd"] for r in rows if (r["degraded"] or "") != "") + by_ok0 = sum(r["cost_usd"] for r in rows if r["ok"] == 0 and r["cost_usd"] > 0) + + free = [r for r in rows if r["tm_hit"] == 1] + per_attempt = collections.Counter(attempt_of(r) for r in rows) + return dict(rows=len(rows), calls=len(rows) - len(free), free=len(free), total=total, + units=len(units), paid_units=len(paid), n1=len(n1), n1_money=n1_money, + n2=len(n2), n2_but_first=but_first, n2_but_last=but_last, + by_degraded=by_degraded, by_ok0=by_ok0, per_attempt=dict(per_attempt), + n1_keys=sorted(n1), n2_keys=sorted(n2)) + + +def report(m, label): + def pct(x): + return f"{100 * x / m['total']:.1f}%" if m["total"] else "n/a" + print(f"=== {label} ===") + print(f" rows={m['rows']} calls={m['calls']} free checkpoint replays={m['free']} " + f"total=${m['total']:.6f} per attempt (from trace_id): {m['per_attempt']}") + print(f" UNITS total={m['units']} paid (sum cost>0)={m['paid_units']} " + f"{'⚠ the paid filter removed nothing — its behaviour is still unexercised' if m['units'] == m['paid_units'] else 'the paid filter DID remove units'}") + print(f" NUMERATOR 1 — a paid attempt that was not kept: {m['n1']} of {m['paid_units']} units = " + f"{100 * m['n1'] / m['paid_units']:.1f}% of units (money ${m['n1_money']:.6f} = {pct(m['n1_money'])})") + for k in m["n1_keys"]: + print(f" {k}") + print(f" NUMERATOR 2 — a cell whose KEPT result was paid for more than once: {m['n2']} units") + print(f" all payments but the FIRST = ${m['n2_but_first']:.6f} = {pct(m['n2_but_first'])} <- the declared number (lower bound)") + print(f" all payments but the LAST = ${m['n2_but_last']:.6f} = {pct(m['n2_but_last'])} <- the other orientation, printed beside it") + for k in m["n2_keys"]: + print(f" {k}") + print(f" MONEY, path 1 (degraded <> '') = ${m['by_degraded']:.6f} = {pct(m['by_degraded'])}") + print(f" MONEY, path 2 (ok=0 AND cost>0) = ${m['by_ok0']:.6f} = {pct(m['by_ok0'])}") + print(f" {'the two paths AGREE — a weak sign: on run A they agree BY CONSTRUCTION' if abs(m['by_degraded'] - m['by_ok0']) < 1e-9 else '⛔ the two paths DISAGREE — a STRONG finding: the classifier and the acceptance mark have come apart'}") + + +SELFCHECK = {"rows": 33, "calls": 27, "free": 6, "units": 17, "paid_units": 17, "n1": 4, "n2": 6} +SELFCHECK_MONEY = {"total": 0.419423, "by_degraded": 0.106472, "by_ok0": 0.106472, + "n2_but_first": 0.028742, "n2_but_last": 0.035723} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--db", required=True) + ap.add_argument("--label", default="run") + ap.add_argument("--selfcheck", action="store_true", + help="the db is run A's: refuse unless every published figure of run A comes back") + ap.add_argument("--json") + a = ap.parse_args() + m = measure(load(a.db)) + report(m, a.label) + if a.json: + with open(a.json, "w") as f: + json.dump({k: v for k, v in m.items() if k not in ("n1_keys", "n2_keys")}, f, indent=2) + if a.selfcheck: + bad = [f"{k}: {m[k]} != {v}" for k, v in SELFCHECK.items() if m[k] != v] + bad += [f"{k}: {m[k]:.6f} != {v:.6f}" for k, v in SELFCHECK_MONEY.items() + if abs(m[k] - v) > 5e-7] + if bad: + print("⛔ SELFCHECK FAILED — this instrument does not reproduce run A:") + for b in bad: + print(" " + b) + sys.exit(1) + print("SELFCHECK PASSED: every published figure of run A comes back from its own database.") + + +if __name__ == "__main__": + main()