Freeze the second door-to-file run before any money: what it buys, the measure it will answer, the limits it declares and the instruments that take them
This commit is contained in:
parent
2792058552
commit
4ee7f03a79
4 changed files with 869 additions and 0 deletions
208
docs/experiments/25-door-to-file-b.md
Normal file
208
docs/experiments/25-door-to-file-b.md
Normal file
|
|
@ -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 сверена
|
||||
до и после). Пустая база про данные не говорит ничего; эта — говорит.
|
||||
26
eval/cold_run_b/README.md
Normal file
26
eval/cold_run_b/README.md
Normal file
|
|
@ -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
|
||||
(«два моих инструмента собраны не из фриза») здесь закрыта построением.
|
||||
508
eval/cold_run_b/door.py
Executable file
508
eval/cold_run_b/door.py
Executable file
|
|
@ -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()
|
||||
127
eval/cold_run_b/measure.py
Normal file
127
eval/cold_run_b/measure.py
Normal file
|
|
@ -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-<run>-<n>)."""
|
||||
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()
|
||||
Loading…
Add table
Reference in a new issue