# Закон раскрытия — дизайн на ратификацию (фаза 1 пака «ДЕНЬГИ И ЧЕСТНОСТЬ ВЫДАЧИ») > **СТАТУС: РАТИФИЦИРОВАН ВЛАДЕЛЬЦЕМ 31.08 — нота D39.181.** Норма зоны. Ратифицированы три вещи: сам > закон · поля леджера доставки в кадре `finished` (A5) · `config_drift_basis` в `status --json` (A7/A12). > **Все десять экземпляров корпуса ПРИМЕНЕНЫ** — разбор по пунктам в `MONEY_HONESTY_REPORT.md` §9. > ⚠ Оговорка к Ст. 3 (дешевизна полосы 10–19) дописана ПОСЛЕ ратификации по находке оркестратора и стоит > ниже; она сужает полномочие, а не расширяет его. > ⚠ Честная цена, названная при ратификации: **закон загейчен примерно на 40%** — см. §7. > > Написан бэкенд-сессией `textmachine-main-8a` (промт — `docs/archive/prompts/BACKEND_MONEY_HONESTY_SESSION_PROMPT.md` §1), > ратифицирован владельцем через оркестратора №21. > ⚠ **Каждый факт ниже снят ИСПОЛНЕНИЕМ на HEAD `9d27c0b`**, командой, названной рядом. --- ## 0. Что это и чего это не **Это** — правило вида «факт КЛАССА X, вычисленный движком, обязан достичь потребителя Y по каналу Z», плюс механический гейт на его нарушение. **Это НЕ** — каталог полей, не расширение шва и не новое слово ни в одном ратифицированном словаре. Там, где закон ТРЕБУЕТ расширения, он это называет отдельным пунктом на ратификацию (§7). **Проверка качества — на корпусе:** закон обязан объяснить все десять строк таблицы промта и назвать канал каждой. §5 это делает. §6 предъявляет **ОДИННАДЦАТЫЙ экземпляр**, которого в таблице нет. --- ## 1. Болезнь — одним предложением, и почему она не десять багов > **Движок вычисляет факт и не доносит его до того, чьё решение от этого факта зависит** — молча, ложью > о причине, или в канал, который этот потребитель не читает. Десять экземпляров — не десять багов, а один отсутствующий закон: **у движка нет дисциплины «что потребитель обязан узнать и по какому каналу»**. Чинить поштучно значит заказать одиннадцатый. ⚠ **И закон не импортируется извне — он уже наполовину написан В ЭТОМ КОДЕ, просто применён точечно.** Две нормы живут в комментариях и не имеют ни имени, ни гейта: 1. **«unknown, not zero»** — `internal/pipeline/quality.go:233`=`the UNSIGNED BANK count is unknown, not zero`, `internal/pipeline/status.go:768`=`the unsigned-term count is unknown, not zero`, `internal/pipeline/status.go:851`=`reported as unknown, not as none`, `internal/pipeline/bookbuild.go:227`=`(reported as unknown, not as none)`. 2. **Фигура едет С БАЗИСОМ** — `RebillBasis` (`internal/pipeline/status.go:275`=`RebillBasis string`), четыре значения `pending|stored|none|failed` (`internal/pipeline/bankmaterialize.go:360-363`), и её докстринг формулирует ровно закон: *«RebillBasis says WHAT the two figures above are a projection of, because the number alone cannot carry that and a reader must never have to guess … failed — the figures are zero because they are UNKNOWN.»* **§2.3 закона ниже есть ОБОБЩЕНИЕ `RebillBasis` на все опубликованные фигуры и флаги** — на этой одной статье приёмке проще всего: она не вводит новой философии, а даёт имя и гейт тому, что зона уже признала правильным на одном поле и не распространила на остальные. ⚠ **И не больше того — разделение своего и заимствованного (ратифицировано, D39.181 §2).** У §2.1 и §2.2 родословная другая — норма «unknown, not zero» и дисциплина причин; §2.4 и §2.5 родословной в этом коде НЕ имеют вовсе, это НОВЫЕ нормы, и приёмка обязана судить их как новые. ⚠ И сам `RebillBasis` богаче §2.3: он ЧЕТЫРЁХзначен, и три его значения (`pending`/`stored`/`none`) означают «факт известен», различая ИСТОЧНИК проекции, — то есть §2.3 берёт от него только ось «известно/не установлено» и не воспроизводит ось источника. --- ## 2. Потребители — перечень снят ГРЕПОМ, а не принят с промта Промт (§1) велел проверить перечень самой: «я его уже один раз назвал неверно». Проверила. | потребитель | что решает | что ЧИТАЕТ (проверено) | |---|---|---| | **Оператор** | править ли ВХОД: конфиг, промпт, исходник, ключи, потолки | stdout/stderr (`log/slog`), вывод `translate`, `report`, `build` | | **Платформа** | в какое состояние перевести прогон · сколько списать · респавнить ли | РОВНО пять каналов, §3 | | **Полигон / замер** | доверять ли этому экспорту как измерению | `tmctl export --json` (контракт слоя 6, `backend/README.md` инвариант 8) | | **Владелец-покупатель** | покупать ли дальше | через платформу; **сумм на экране не видит** (D39.84) | ⚠ **Оператор и платформа — НЕ один потребитель с разной вежливостью.** Они читают физически разные байты, и ни один канал не общий: **платформа не читает прозу движка нигде.** Единственное касание — ПЕРВАЯ СТРОКА stderr, вклеиваемая в текст ошибки исполнителя (`platform/internal/runner/engine.go`, греп `firstLine(errOut.Bytes())`), и это диагностика для человека, а не канал факта. --- ## 3. Каналы — замкнутая таблица, снята с кода ОБЕИХ зон ### 3.1 Что платформа декодирует (`platform/internal/`, read-only) | канал | тип на приёме | что реально берётся | |---|---|---| | код выхода | `ingest.OutcomeOf` (`platform/internal/ingest/exit.go`) | 0·1·2·3·4·5 + ПОЛОСА 10–19; **неизвестный код → `OutcomeFailed`** | | `status --json` | `ingest.StatusReport` (`platform/internal/ingest/resync.go:20`) | **аллоулист 12 ключей**: `book_id · total_units · done · in_progress · flagged · pending · progress{draft,edit} · eta_seconds · unsigned_bank_terms · committed_usd · reserved_usd · chapters[]`. ⚠ И УЖЕ: продакшн-читателя имеют **ПЯТЬ** — `flagged` (только как подтверждение кода 2) · `progress.draft.total` + `progress.edit.total` · `eta_seconds` · `committed_usd` · `reserved_usd`. Остальные декодируются и не читаются никем вне тестов | | `manifest --json` | `ingest.Manifest` | дерево глав/юнитов + `artifacts` **с ОДНИМ полем** — `bank_export` (`platform/internal/ingest/manifest.go:53-57`) | | `export --json --pairs` | `ingest.Export` | пары для читателя | | `events.jsonl` | 7 типов кадров | `hello·progress·unit_done·bank_stop·ceiling·spend·finished` | | `bank-apply` stdout | `ingest.BankReport` | решения по термам | **Чего платформа НЕ берёт, хотя движок это печатает** (снято сверкой двух структур поле-в-поле): `config_drift` · `snapshot_drift` · `current_snapshot` · `snapshot_id` · `rebill_units` · `rebill_usd` · `rebill_output_units` · `rebill_basis` · `projected_book_usd` · `book_ceiling_usd` · `ceiling_pct` · `stages_skipped` · `escalations` · `postcheck_misses` · `style_flags` · `content_labels` · `routing` · `artifacts.book_files` · `artifacts.project_db` · `artifacts.mined_delta` · `artifacts.mined_rejects` · `chapters[].worst_flag_reason` · `chapters[].cost_usd` · `chapters[].verdict`. ⚠ **Два следствия, которые меняют диспозиции пака, и оба против промта:** - **`config_drift` платформе НЕ ВИДЕН.** ⇒ A7 — факт ОПЕРАТОРА и ПОЛИГОНА, не платформы. - **`artifacts.book_files` платформа НЕ ДЕКОДИРУЕТ ВООБЩЕ.** У неё `StatusArtifacts` из одного поля. ⇒ A3(б) как «платформа обманута» **не существует** (разбор — §5). ### 3.2 Что движок вообще умеет сказать (`backend/`) Пять родов каналов и больше никаких: **код выхода · stdout-документы · файлы-артефакты рядом с БД · stderr-логи `log/slog` · ничего больше.** ### 3.3 ⛔ Словари СВАРЕНЫ — и это решает A5 `platform/internal/ingest/exit.go` дословно: *«Outcome is the engine's own verdict about a run, and it is deliberately readable from EITHER channel: it is what the terminal `finished` line carries and what OutcomeOf reads off an exit code. The two are one vocabulary because they are one fact travelling twice.»* Значения помечены номерами: `clean`=0 · `failed`=1 · `flagged`=2 · `bank_stop`=3 · `ceiling`=4 · `stopped`=5. ⚠ **Довод «пара БИЕКТИВНА» НЕВЕРЕН и не годится** (D39.181 п.2 стоит на другом): `OutcomeRefused` уже существует и counterpart'а в потоке НЕ имеет — та же константа, дословно: *«OutcomeRefused is the band, and it has no `finished` counterpart: an invocation that was turned down did no work, so the engine writes no verdict about work»*. Асимметрия уже узаконена, и аргумент «так нельзя, потому что нельзя» падает. **Точный аргумент — другой.** Асимметрия сегодня односторонняя: у КАЖДОГО значения `finished.outcome` есть код-двойник, а у кодовой стороны есть одно лишнее. Значение, живущее ТОЛЬКО в потоке, было бы первым в обратную сторону — и тогда для ОДНОГО прогона два канала назвали бы РАЗНЫЙ исход: поток сказал бы `volume`, а `OutcomeOf(0)` на том же прогоне — `clean`. Это ровно то, что запрещает сама константа («one fact travelling twice»): не асимметрия, а РАСХОЖДЕНИЕ. ⇒ **Признак остановки по объёму не может быть словом ни в одном из двух словарей** — не потому, что словарь заперт, а потому, что слово в одном канале сделало бы каналы противоречащими. Он обязан быть ЧИСЛОМ в поле: у чисел словаря нет, и расходиться нечему. --- ## 4. ЗАКОН — четыре статьи ### Ст. 1. Что такое подотчётный факт Факт **ПОДОТЧЁТЕН**, если выполнены оба условия: * **(а) движок его ЗНАЕТ** — значение лежит в памяти процесса в момент решения, ЛИБО выводится за $0 из строк, которые движок уже записал (леджер · `chunk_status` · `checkpoints` · `snapshots` · `events_outbox`); * **(б) есть потребитель, чьё СЛЕДУЮЩЕЕ ДЕЙСТВИЕ от него меняется** (§2). Условие (б) — это и есть ограничитель: закон не требует печатать всё, что известно. Он требует печатать то, **без чего кто-то примет другое решение**. ### Ст. 2. Четыре обязанности. Ни одна не заменяет другую **§2.1 — НЕ МОЛЧАТЬ.** Подотчётный факт обязан быть на канале своего потребителя. *Экземпляры: A0 · A3б · A5 · A6 · A10.* **§2.2 — НЕ НАЗЫВАТЬ ПРИЧИНУ, КОТОРУЮ НЕ СРАВНИВАЛ.** Формально: **сообщение, утверждающее причину C, обязано производиться на площадке, которая держит ОБА операнда сравнения, устанавливающего C, и обязано их напечатать.** Причина, не сравнённая на месте, не называется вовсе — «снапшот разошёлся» честно, «конфиг/промпты изменились» ложно. *Экземпляры: A4.* **§2.3 — ФИГУРА ЕДЕТ С БАЗИСОМ. (Обобщение `RebillBasis`.)** **Опубликованная фигура или флаг, чьё значение может быть произведено И самим фактом, И неудачей установить факт, обязаны нести поле-БАЗИС, различающее эти два случая.** Ноль, который значит и «нет», и «не знаю», — ложь по построению, и стоит она денег: `false` в `config_drift` читается как «строки актуальны». *Экземпляры: A7 · A2 · **A12 (одиннадцатый, §6)**.* **§2.4 — КАНАЛ НАЗНАЧАЕТ ПОТРЕБИТЕЛЬ, А НЕ УДОБСТВО ПИШУЩЕГО.** ⚠ **Отношение к §2.1 названо, а не оставлено читателю:** §2.4 — не независимая обязанность, а ПРОЦЕДУРА исполнения §2.1. §2.1 говорит «факт обязан быть на канале своего потребителя»; §2.4 говорит, КАКОЙ это канал и как его выбрать. Строка корпуса нарушает §2.1 всегда, когда нарушает §2.4; отдельный номер у §2.4 есть потому, что нарушают его по-разному — молчанием (§2.1) или громкой публикацией не туда (§2.4), — и лечатся они разным. **Перечни «экземпляров» под §2.1 и §2.4 поэтому ПЕРЕСЕКАЮТСЯ намеренно, и §5 называет для каждой строки ту, которая ведёт к правке.** * Проза (stdout/stderr, секция `report`) — канал **ОПЕРАТОРА и только его.** * Факт, нужный **платформе**, обязан ехать в канале, который `platform/internal/ingest` ДЕКОДИРУЕТ (§3.1). «Признак есть в отчёте прогона» задачу не решает — отчёт платформе не виден. * Факт, нужный **замеру**, обязан ехать в `export --json`. * Факт, нужный ДВОИМ, едет ДВАЖДЫ, и обе копии обязаны производиться из ОДНОГО вычисления. *Экземпляры: A5 · A7 · A8.* **§2.5 — НЕОБРАТИМОЕ ОТЧИТЫВАЕТСЯ РАНЬШЕ ПОСЛЕДУЮЩЕЙ ОШИБКИ.** Совершив необратимое — потратив деньги, записав файл, удалив файл, — движок обязан **сначала** выдать отчёт об этом и **только потом** сообщать об ошибке, случившейся позже на том же пути. Ошибка ПОСЛЕ необратимого действия не смеет ехать маршрутом, который отчёт подавляет. *Экземпляры: A3а.* ### Ст. 3. Расширение каналов — ЧЕТЫРЕ класса, и они РАЗНОЙ цены | класс | что это | кто вправе | |---|---|---| | **НОВОЕ ЧИСЛО в существующей форме** | ещё одна строка разложения в `report`, ещё один атрибут лога | сессия зоны | | **НОВОЕ ПОЛЕ в опубликованном документе** | поле в `status --json` / кадре потока; аддитивно и обратно-совместимо (`encoding/json` игнорирует незнакомые), но это КОНТРАКТ ШВА | ратификация ОРКЕСТРАТОРА | | **НОВОЕ СЛОВО в замкнутом словаре** | значение `Outcome` · тип кадра · значение `flag_reason`, пересекающее шов · код выхода ВНЕ полосы (0–5) | ратификация ВЛАДЕЛЬЦА | | **НОВЫЙ КЛАСС ОТКАЗА ВНУТРИ полосы 10–19** | новая константа в `refusalExit` + `pipeline.RefusalClass`; незнакомый номер у потребителя поглощается ЧЛЕНСТВОМ в полосе и читается «refused», а не «failed» — полоса спроектирована ровно под это | ратификация ОРКЕСТРАТОРА (прецедент — код 13) | ⚠ **Коды выхода НЕ валятся в один класс «слово владельца» — ПОЛОСА растёт иначе:** `platform/internal/ingest/exit.go` дословно — *«It is a BAND and not a list because the vocabulary is the engine's and it will grow. A consumer looks the number up; a class this build has never heard of still lands inside the band and reads as "refused"»*, а `17-seam-inbound-law.md` называет прецедент: код **13** встал в полосу «без нового решения ВЛАДЕЛЬЦА», через ратификации ОРКЕСТРАТОРА (D39.131 п.2а → D39.132 п.2г → D39.134). Полоса спроектирована расти; заморожена верхняя шестёрка 0–5. ⚠ Для A2 это ничего не меняет: полоса значит «ничего не потрачено», а фаза идёт после оплаченной черновой волны. ⛔ **ОГОВОРКА К ДЕШЕВИЗНЕ ПОЛОСЫ — она из находки A15 (§6), и без неё закон разрешает удешевлённо делать то, что сам же называет опасным.** Довод, которым полоса удешевлена, — «незнакомый номер поглощается ЧЛЕНСТВОМ и читается `refused`, а не `failed`». Это верно **по оси НОМЕРА и только по ней.** A15 показала, что разошёлся не номер, а **ОБЕЩАНИЕ** полосы: движок снял с неё «ничего не записано», платформа этот пункт держит. Поглощение членством спасает от НЕЗНАКОМОГО ЧИСЛА и не спасает от РАСХОДЯЩЕЙСЯ СЕМАНТИКИ: потребитель применит к новому классу ту гарантию, которую держит ОН, а не ту, которую даёт производитель. По `PD-196` он применяет её разрушительно. ⇒ **Норма:** рост полосы дёшев по оси номера и НЕБЕЗОПАСЕН по оси обещания, пока копии словаря расходятся. **Пока строка бэклога 246 открыта, новый класс вводится в полосу ТОЛЬКО вместе со сверкой обеих копий** (движок `cmd/tmctl/main.go` ↔ платформа `internal/ingest/exit.go`), и сверка предъявляется в том же паке. Закроется 246 — оговорка снимается вместе с ней, потому что её предмет исчезнет. ⚠ И общее следствие, которое стоит дороже самой оговорки: **дешевизна класса расширения — свойство не СЛОВАРЯ, а его КОПИЙ.** Любой словарь, у которого копий больше одной и нет машинной сверки между ними, дёшев только на бумаге. Это ревью-вопрос к каждому будущему расширению, а не разовая заплатка. **Правило выбора: бери самый дешёвый класс, которым факт выражается без лжи.** A5 — ровно применение этого правила: слово нельзя, число можно, значит число. ### Ст. 4. Закон без гейта гниёт Каждая статья обязана иметь машинную проверку, краснеющую на СЛЕДУЮЩЕМ экземпляре, а не на уже известных. Дизайн гейта — §7. Статья, для которой гейт не построен, объявляется НЕЗАГЕЙЧЕННОЙ вслух: незагейченная норма — это норма, которую следующая сессия унаследует как факт и нарушит молча. --- ## 5. Корпус: все десять строк, разобранные законом ⚠ **ЧИТАТЬ КАК ДИАГНОЗ ДО ПРАВКИ, А НЕ КАК ОПИСАНИЕ СЕГОДНЯШНЕГО КОДА.** Все десять применены (D39.181/D39.182), поэтому каждая фраза «**Делает:** …» ниже описывает состояние ДО лечения и сегодня ложна как утверждение о коде; что стало — `MONEY_HONESTY_REPORT.md` §9 (по одному экземпляру, с мутацией и её дословным выводом). Секция оставлена целиком по двум причинам: она и есть проверка качества закона на корпусе (§0), и она несёт названные ЛОВУШКИ и опровержения, которые правки учли и которые переоткрывать не надо. Формат: **что движок ЗНАЕТ** (с якорем) → **какая статья нарушена** → **потребитель** → **канал** → **класс правки по ст. 3**. ### A0 — оплаченный хвост неудач невидим * **Знает:** `EscalationSpentUSD` (`internal/store/ledger.go`, греп `func (s *Store) EscalationSpentUSD`), колонки `checkpoints.attempt` · `checkpoints.escalation` · `request_log.ok` · `request_log.estimated`. * **Делает:** деньги публикуются ИТОГОМ (`committed_usd`), разложения «за что заплачено впустую» нет ни в `status`, ни в `report`. * **Нарушено:** §2.1. * **Потребитель:** ОПЕРАТОР (правит модель/потолок/промпт). Платформа сегодня — НЕТ: она не берёт даже `rebill_*`, а суммы на экран запрещены (D39.84). * **Канал:** секция `report` (класс «новое число» — сессия вправе). Поле `status --json` — класс «новое поле» ⇒ ратификация, и я его НЕ рекомендую первым шагом: у него нет потребителя. * ⚠ **И собственная ловушка среза, замеренная мной на живых деньгах:** `ok=0 AND cost_usd>0` **НЕ ЕСТЬ «оплачено впустую»**. На `coldrun-v16` в этих 11 строках ($0.12316089) лежат три РАЗНЫЕ вещи: `degraded=cjk_artifact` у роли **classifier** — 3 строки, $0.00918827, **мис-вердикт**, вызовы удались. ⚠ Обоснование — КОДОМ, а не строкой бэклога 105: её предпосылка «`classify_types` выключен во всех шиппинг-конфигах» для ЭТОГО прогона ЛОЖНА — он его включал. Механизм ТОГДА: исключение только терминологу; СЕГОДНЯ — обоим банк-ролям (`terminologist.go`, `isBankRole`) — исключение из эхо-правила выдано ТОЛЬКО терминологу, хотя формат ответа классификатора — та же двуязычная таблица терминов, а докстринг самого поля объясняет, чем это кончается: *«Left on, every healthy call of that role lands in request_log as ok=0/degraded=cjk_artifact — poisoning the one signal that says a provider is misbehaving»*. ⚠ **И ПРЯМАЯ улика на сами эти три строки, а не только форма кода:** их ответы лежат в `checkpoints.response_text`, читаются read-only и здоровы — разбор с числами и содержимым `MONEY_HONESTY_REPORT.md` §5.4. ⇒ вызовы удались, вердикт ложен. ⛔ **Чинить это здесь НЕЛЬЗЯ:** правка применимости эхо-правила двигает `classifierVersion`, а он — поле снапшота (`snapshot.go`, греп `ClassifierVersion versions the intrinsic`) ⇒ ярус B (и ровно это записано ценой в строке бэклога 105); `degraded=sanitizer_stripped` у редактора — 1 строка, $0.01865424, текст **ОТГРУЖЕН**; и лишь $0.09531838 действительно выброшено. **Наивная поверхность назвала бы оператору на 29.2% больше потерь, чем было** ($0.12316089 против истинных $0.09531838) — то есть сама была бы экземпляром §2.2. ⇒ Разложение обязано печатать не «впустую», а КЛАССЫ: `отброшено · отгружено-с-флагом · мис-вердикт`. ### A2 — фаза стартует, зная, что не влезает * **Знает:** суммарную смету всех батчей ДО первого вызова и бюджет в той же области видимости — `internal/pipeline/terminologist.go`, греп `estimate before any call`. * **Делает:** печатает `estimate_usd` и `budget_usd` рядом, следующий оператор — проверка ошибки ЧТЕНИЯ бюджета, **сравнения нет**; обрывается на середине пер-батчевым гейтом, оставив оплаченную частичную работу. * **Нарушено: §2.3.** Частичный результат подаётся как результат, без базиса «пасс оборван бюджетом». ⚠ **§2.5 здесь НЕ нарушен.** §2.5 — правило ПОРЯДКА ОТЧЁТА, а фаза как раз отчитывается: пер-батчевый гейт печатает WARN, НАЗЫВАЮЩИЙ необратимую трату (`spent_usd`, `next_batch_usd`, `batches_left`), и только потом `break` — ошибка не возвращается, отчёт не подавляется. Похожее на §2.5 здесь — «решение принято позже, чем могло быть», а это ДРУГОЕ правило, и его в законе нет. **Заводить его я не стала: одна строка корпуса — недостаточное основание для пятой обязанности, и честнее оставить дыру названной.** * **Потребитель:** оператор (поднять бюджет) + деньги. * **Канал:** существующий — план фазы усекается до влезающего ДО первого вызова, и строка называет усечение. ⛔ **Отказ классом `Refusal` ЗАПРЕЩЁН** и это подтверждено дословно: `platform/internal/ingest/exit.go` о полосе 10–19 — *«the invocation was turned down BEFORE it did any work of its own — nothing reached a provider, nothing was spent»*. Фаза идёт ПОСЛЕ оплаченной черновой волны ⇒ отказ этим классом сделал бы движок лжецом о деньгах. * **Класс правки:** новое число (сессия вправе). ### A3(а) — `build` возвращает exit 1 при уже лежащих файлах * **Знает:** полный корректный `*BuildReport` в памяти и то, что файлы закоммичены. * **Делает:** сбой уборки незапрошенного формата возвращается ГОЛОЙ ошибкой (`internal/pipeline/bookbuild.go`, греп `remove the previous %s copy`) ⇒ маппер даёт `default: return 1` (`backend/cmd/tmctl/main.go`, греп `func exitCode`), отчёт не печатается. Потребитель по контракту полосы решает «инфра-сбой, файлов нет» и списывает прогон. * **Нарушено:** §2.5 — в чистом виде. * **Потребитель:** оператор + платформа (код выхода). * **Канал:** существующий. Отчёт печатается, сбой уборки становится WARN. ⚠ **В том же файле уже стоит честный образец для СОСЕДНЕГО сбоя** — `RefusalWriteIncomplete` с перечислением `landed:` (греп `the copies were prepared and the write did not complete`). Правка — распространить его на уборку. * **Класс правки:** новое число (сессия вправе). ### A3(б) — `book_files` печатает путь удалённого файла * **ВЕРДИКТ: ОПРОВЕРГНУТО как дефект `book_files`, и подтверждено как ДРУГОЙ дефект.** * Опровержение — двумя цитатами, обе снятые исполнением: движок, `internal/pipeline/status.go` (греп `names a PLACE, not a presence`): *«Every path here names a PLACE, not a presence … Publishing "does it exist" instead would be a fact about a moment that has passed by the time the consumer reads it, and would invite the check-then-open race»*; и `BookFiles` — *«A PLACE like the others — the map is complete whether or not a build has run, and a consumer opens the path and handles not-found»*. Платформа, `platform/internal/ingest/manifest.go:53-56` — та же фраза о `bank_export`: *«The path names a PLACE, not a presence»*. **Плюс платформа `book_files` не декодирует вовсе.** ⇒ контракт написан, обеими сторонами, с доводом (гонка check-then-open). Обманутого нет. * **Настоящий дефект в том же месте и он НАСТОЯЩИЙ:** `build --format epub` **МОЛЧА удаляет** соседний `.book.txt`. `os.Remove`, вернувший `nil`, ЗНАЧИТ «файл был и его больше нет» — движок различает это от «файла не было» (`fs.ErrNotExist`) и **выбрасывает различение**, не сказав ни строки. * **Нарушено:** §2.1 (и §2.5 — удаление необратимо). * **Потребитель:** оператор. **Канал:** строка сборки. **Класс:** новое число. ### A4 — снапшот-гард называет причину, которой не было * **Знает:** ОБА payload'а. `snapshots` хранит `payload TEXT NOT NULL` на каждый ID (`internal/store/migrate.go`, греп `CREATE TABLE IF NOT EXISTS snapshots`), а `buildSnapshotID` возвращает `(id, payload string)`. ⇒ **пофайловый JSON-диф выводим за $0.** * **Делает:** `internal/pipeline/stagerun.go` (греп `the config/prompts changed`) утверждает причину, которую не сравнивал. * **ПРЕДЪЯВЛЕНО НА ЖИВЫХ ДАННЫХ ПРОГОНА** (read-only, `sqlite3 'file:…?mode=ro'`): два edit-снапшота `f5258345…` и `c8d74bbaef…` различаются **РОВНО ОДНИМ ключом — `memory_version`**; `prompt_sha256`, `model`, `temperature`, `reasoning` и все прочие побайтно равны. Гард сказал бы «конфиг/промпты изменились» — и это ложь, доказанная его же хранилищем. * **Нарушено:** §2.2. * **Потребитель — ДВОЕ, не «только оператор»** (проверено по реестру платформы). `PD-422` дословно: движковый джоб-гард останавливает прогон, и платформа видит **`exit 1` ⇒ `failed`** — то есть ложную причину читает не только человек, причём резолвится она как «инфра-сбой», а не как «нужен `--resnapshot`». ⚠ Строка помечена там же как сегодня БЕСПРЕДМЕТНАЯ: проводка `--max-units` на платформе загейчена, и вторая покупка этой формы пока не возникает. * **Канал:** оператору — текст гарда (stderr, класс «новое число»); платформе — отдельный вопрос, и он НЕ этого пака: её половина уже заведена строкой `PD-422` в чужой зоне. * ⚠ **Ловушка для исполнителя (проверена по коду):** готового дифа полей НЕТ. `classifySnapshotMove` (`repin.go`) ходит по КАРТЕ и делает `return moveOther` на ПЕРВОМ несовпавшем ключе — он не собирает МНОЖЕСТВО разошедшихся полей, а какой ключ встретится первым, у Go-мапы недетерминировано. Диф для сообщения обязан быть НОВЫМ и ДЕТЕРМИНИРОВАННЫМ (сортировка ключей), а не этим циклом. * **Класс правки:** новое число. ### A5 — остановка по объёму не имеет машинного носителя * **Знает:** целую структуру `VolumeStop{MaxUnits · Delivered · Reworked · Flagged · Free · LeftFresh · LeftRework}` (`internal/pipeline/volume.go`, греп `LeftFresh is undelivered units`). * **Делает:** публикует её ОДНОЙ ПРОЗАИЧЕСКОЙ СТРОКОЙ. Кода выхода у объёмного стопа нет вовсе, а `finished.outcome` он получает `clean` либо `flagged` — ровно те же, что книга, дочитанная до конца (`internal/pipeline/events.go`, греп `func (e \*emitter) terminal`). * **Нарушено:** §2.4 — факт платформы едет прозой. * **⚠ ДВЕ ПОПРАВКИ К ПРОМТУ, обе проверены исполнением:** 1. **Платформа сегодня `--max-units` НЕ ШЛЁТ ВООБЩЕ** (`platform/internal/runner/engine.go`, греп `func TranslateArgs`; во всей зоне `platform/` вне комментария этого флага нет). ⇒ A5 — дефект БУДУЩЕГО потребителя, не живой инцидент. Срочность падает, дизайн — нет. 2. Формулировка «неотличима от «что-то тихо сломалось»» **слишком сильна**: тихий слом даёт 1 или сигнал, а 0/2 по контракту значат «команда СДЕЛАЛА свою работу» (`platform/internal/ingest/exit.go`, греп `func CompletedWithFlags`). **Неразличимы другие две вещи:** «остановился, потому что грант кончился» и «дочитал книгу до конца» — и вместе с ними теряется ВЕСЬ леджер доставки, включая ось `LeftFresh`/`LeftRework`, которая и есть «что ещё можно продать» (родня строки 232). * **Канал — рекомендация с доводом:** **ЧИСЛА, а не слово.** По §3.3 слово в `Outcome` разварило бы пару «код выхода ↔ кадр», а кода заводить нельзя. Числа словаря не имеют. Форма: кадр `finished` получает поля леджера доставки (`max_units · delivered · reworked · flagged · free · left_fresh · left_rework`). ⛔ **ЛОВУШКА ПОТРЕБИТЕЛЯ (проверена по коду).** Напрашивающийся предикат `delivered+reworked == max_units && left_fresh+left_rework > 0` **ЛОЖЕН ровно на том прогоне, ради которого A5 заведён:** `reconcile()` (`volume.go`) ПОСЛЕ волн переносит оплаченный, но флагнутый юнит ИЗ `Delivered`/`Reworked` в `Flagged`, а улика A5 — это `--max-units 3` с одним флагнутым чанком, где `delivered+reworked = 2 ≠ 3`. Потребитель прочёл бы объёмный стоп как обычный прогон — ровно та путаница, которую A5 убирает. ⇒ **правильный предикат считает ОПЛАЧЕННОЕ вместе с флагнутым** — у движка это уже есть: `VolumeStop.Paid()` плюс `Flagged`, а «сколько осталось» — `Left()`. То есть поле должно ехать так, чтобы потребителю не приходилось складывать его самому: **если поля всё равно новые, честнее послать ГОТОВЫЙ признак числом (`max_units` и `left_*`), а не заставлять внешнюю сторону воспроизводить внутреннюю арифметику `reconcile`.** Это и есть окончательная рекомендация. * **Класс правки: НОВОЕ ПОЛЕ ⇒ ратификация оркестратора. Сессия вносить не вправе — это ПИНГ.** ### A6 — деньги слепы к правке исходника на месте (строка 238) * **Знает:** `chunk_status.ContentHash` и умеет пере-рендерить текущий контент-хеш — `runStage`'s resume fast-path сверяет `cs.ContentHash == contentHash` (`internal/pipeline/stagerun.go`, греп `cs.ContentHash == contentHash`). * **Делает:** `projectRebill` в ветке НЕсдвинутого снапшота выходит по `if cs.SnapshotID == cur { continue // resumes at $0 }` (`internal/pipeline/rebill.go`) — **без сверки контента.** Сверка сделана только в bank-only ветке. ⚠ И докстринг функции при этом утверждает: *«The projection now models the resume predicate the run actually applies»* — утверждение шире кода, что само есть §2.2. * **Нарушено:** §2.1 (смета молчит) + §2.2 (докстринг). * **Потребитель:** оператор (согласие на пере-оплату) + деньги. * **Канал:** СУЩЕСТВУЮЩИЕ поля — `rebill_units`/`rebill_usd`/`rebill_basis`. Носитель уже есть, и это главный довод: правка не расширяет шов вообще. * **Класс правки:** новое число (сессия вправе, после ратификации закона). ### A7 — `status` не видит исчезнувшую стадию, `export` видит * **Знает:** обе поверхности читают одни и те же `chunk_status`. * **Делает:** `export.go` (греп `func (r \*Runner) exportConfigDrift`) имеет ДВЕ проверки — пер-волновую и «строка несёт стадию, которой в конфиге нет» (греп `stored rows carry a stage the current config does not run`). `status.go` (греп `checkWave := func(snaps map\[string\]bool`) имеет только ПЕРВУЮ. * **Нарушено:** §2.4 — один факт, две поверхности, разные ответы. * **Потребитель:** ОПЕРАТОР и **ПОЛИГОН** (он решает по `ConfigDrift`, доверять ли экспорту как измерению). **НЕ платформа** — она `config_drift` не декодирует (§3.1). * **Канал:** существующее поле обеих поверхностей. Правило поднимается в общий хелпер. ⚠ **Что поднимать НЕЛЬЗЯ:** расхождение в свёртке банка между `status` и `export` объявлено СОЗНАТЕЛЬНЫМ (`export.go`, греп `symmetry between two surfaces that answer different questions`). Общим делается ТОЛЬКО правило осиротевшей стадии. * ⛔ **ЛОВУШКА, из-за которой наивная правка A7 СОЗДАЁТ новый экземпляр §2.2:** `projectRebill` пропускает ровно те строки, на которые сработает новое правило — `default: continue // a stage the current pipeline does not run is never re-billed` (`internal/pipeline/rebill.go`). ⇒ поставив `config_drift=true`, поверхность оставит `rebill_units=0`, а человеческий рендер статуса превращает этот флаг в денежное утверждение о пере-оплате, которой не будет. **Правка A7 обязана идти вместе с базисом (§2.3), иначе она чинит одну ложь, заводя вторую.** * **Класс правки:** новое число (сессия вправе) — но НЕ в одиночку, см. ловушку выше. ### A8 — `prompt_version` не отслеживает байты промпта * **Знает:** `PromptSHA256` рядом с `PromptVersion` в payload снапшота, и **payload durable в таблице `snapshots`** — я это пере-снял: дамп payload'а печатает `prompt_sha256` на каждую стадию. * **Делает:** метку не сверяет ни с чем; и ни `status`, ни `report`, ни `export` `prompt_version` не публикуют вовсе (`grep -rn PromptVersion internal/pipeline/{status,export,quality}.go` → пусто). Потребитель метки — ЧЕЛОВЕК, читающий конфиг. * **Нарушено:** §2.3 — метка есть базис без гарантии, и её равенство читается как равенство байтов. * **Потребитель:** оператор/замер (сравнимость прогонов). Не платформа, не деньги (деньги защищены `PromptSHA256` в снапшоте). * ⚠ **И решающий довод: НОРМА УЖЕ ОБЪЯВЛЕНА И УЖЕ НАРУШЕНА.** `backend/configs/pipeline-c2.yaml:56` дословно: «**лейбл обязан следовать за новым SHA файла**». Именно это и произошло 01.08 с `prompts/zh-ru/editor.md` при неизменном `v3-discourse-reflow`. ⇒ гейт здесь — не новый закон, а МАШИНА под уже написанным правилом, которое проза удержать не смогла. Планка ратификации соответственно ниже. * **Канал — рекомендация с доводом:** **репо-гейт, а не рантайм-гейт.** Рантайм-сверка по таблице `snapshots` поймала бы только повтор метки ВНУТРИ одной книги, а исторический инцидент был МЕЖ-КНИЖНЫМ (minirun против coldrun, разные БД) — то есть ровно его она бы и пропустила. Репо-гейт ловит его навсегда: тест над `prompts/**` + конфигами, пиннящий отображение `(пара, роль, prompt_version) → sha256` в `testdata`; правка промпта без бампа метки — красный тест. * **Класс правки:** новое число (сессия вправе). ⛔ Третья форма — метка ПРОИЗВОДНАЯ от хеша — СНАПШОТ-ДВИЖУЩАЯ (`PromptVersion` — снапшотное поле), ярус B, в этом паке не делается. ### A10 — деньги банковых ролей вне контура согласия (строка 194) * **Знает:** чекпоинты терминолога и классификатора со стадией `terminology`, глава 0 — они есть в леджере и в `checkpoints`. * **Делает:** `chunk_status` строк не пишут ⇒ не входят ни в число согласия, ни в `projected_book_usd`; при сдвиге снапшота реально пере-покупаются. * **Нарушено:** §2.1. * **Потребитель:** оператор (согласие) + деньги. * ⛔ **Канал — НЕ «существующие `projected_book_usd` / `rebill_*`», и это проверено на данных.** Обе фигуры считаются ИЗ `chunk_status` (`projectRebill` принимает `statuses []store.ChunkStatus`), а `chunk_status` этого прогона содержит РОВНО draft(20) + edit(3) строк и НИ ОДНОЙ терминологической, тогда как `checkpoints` держит 9 терминологических. ⇒ контур $0.04980482 для этих носителей **структурно невидим**, и положить его туда нельзя, не сменив саму деривацию. **Носитель придётся строить от `checkpoints ⋈ jobs`** (там роль и стадия есть) — то есть A10 дороже, чем «новое число», и это надо знать ДО планирования правки. * **Канал:** новая деривация поверх `checkpoints ⋈ jobs`, публикуемая рядом с `projected_book_usd`. * **Класс правки:** новое число (сессия вправе). ⚠ Замер этого прогона: терминологический контур — $0.04980482 из $0.43610966 = **11.42% захода**, и ни цента из этого в проекцию не входило. * ⛔ **ОСЬ, НАЗВАННАЯ В СТРОКЕ 194 («на сдвиге снапшота они реально пере-покупаются»), ОПРОВЕРГНУТА ЖИВЫМИ ДАННЫМИ — и это самое важное здесь.** Ось пере-покупки — **БАЙТЫ БАТЧА**: батчи пере-собираются, когда растёт множество кандидатов, а оно растёт от каждой новой дочерновленной главы; на оси терминологии снапшот за весь прогон не двигался ни разу. Разложение покупок П1/П2/П3 и прямая улика из таблицы `jobs` — `MONEY_HONESTY_REPORT.md` §5.1 (опровержение ратифицировано D39.181 §3). Чинить по названной в строке оси значит чинить не то. * ⇒ **A10 и A9 — ОДИН дефект с двух сторон**, и §5 A9 ниже несёт его вторую половину. --- ## 6. ОДИННАДЦАТЫЙ ЭКЗЕМПЛЯР — которого в таблице промта нет Промт: «Найдёшь одиннадцатый экземпляр — это лучший результат фазы 1». Нашла ЧЕТЫРЕ. Главные — первые два: A12 внутри движка и A15 на самом шве. ### A12. `config_drift` — двузначное поле для ТРЁХЗНАЧНОГО факта, на ОБЕИХ поверхностях `false` в `config_drift` сегодня значит две несовместимые вещи: **«дрейфа нет»** и **«проверить не удалось»**. Обе ветки предъявлены исполнением: * `internal/pipeline/export.go` (греп `config-drift check failed`) — дословно: `"export: config-drift check failed; drift state unknown (reported as none)"` — **сам код признаёт, что рапортует `none` вместо `unknown`**, и это ЕДИНСТВЕННОЕ место в движке, где фраза «reported as none» стоит там, где у четырёх соседей стоит «reported as unknown, not as none»; * `internal/pipeline/status.go` (греп `config-drift check failed for a wave`) — `return` из хелпера, `rep.ConfigDrift` остаётся `false`; * **и вторая, более тихая ветка у статуса:** `if memBasis != RebillBasisFailed && !rep.SnapshotDrift && …` — при провале свёртки памяти проверка дрейфа **не запускается вовсе**, и поле уезжает `false`. **Почему это лучший экземпляр, а не одиннадцатый по счёту:** 1. Он **вне** заказанной десятки — мерка сработала. 2. Он **того же класса** (§2.3) и потому подтверждает закон, а не расширяет его. 3. Он показывает, что **лекарство у зоны уже изобретено и не разнесено**: у `rebill_*` базис есть (`RebillBasis`, четыре значения, включая `failed` = «ноль, потому что НЕИЗВЕСТНО»), у `config_drift` — нет. Одна и та же болезнь, соседние поля одного документа, разные исходы. 4. **Его потребитель — не тот, кого называет комментарий движка, и настоящий ХУЖЕ.** ⚠ `export.go` (греп `polygon's extraction, which uses ConfigDrift`) утверждает, что потребитель — полигон. **Я это проверила и НЕ подтверждаю:** `grep -rn 'config_drift\|ConfigDrift' eval/` даёт НОЛЬ хитов. Комментарий называет потребителя, которого в дереве нет — сам по себе экземпляр §2.2. **Настоящих потребителей два, оба проверены грепом:** человеческий рендер (`cmd/tmctl/render.go`, два места) и — вот это дорого — **`build`**: `internal/pipeline/bookbuild.go`, греп `func (r \*Runner) staleUnits`, первый оператор тела — `if exp.ConfigDrift { return nil, true }`. ⇒ **тихое `false` пропагируется В СОСЕДНЮЮ честностную поверхность:** `staleUnits` решает, что конфиг чист, и печатает `stale: 0` вместо `stale: UNKNOWN` — то есть **поле БЕЗ базиса портит поле, у которого базис ЕСТЬ.** Точнее сформулировать болезнь нельзя. ### A15. ДВЕ КОПИИ ОДНОГО РАТИФИЦИРОВАННОГО СЛОВАРЯ РАЗОШЛИСЬ — и обещают РАЗНОЕ Это второй по силе экземпляр после A12, и он единственный, который лежит НА САМОМ ШВЕ. **Движок** (`backend/cmd/tmctl/main.go`, греп `The refusal band`) дословно: > «A code in [refusalFirst, refusalLast] means the invocation was TURNED DOWN: nothing reached a provider, > nothing was spent, no work needs rolling back and a retry is safe … **«Nothing was written» is NOT the > band's promise any more**, it is a clause of the individual classes: exit 15 (write incomplete) > legitimately answers with files on disk». **Платформа** (`platform/internal/ingest/exit.go`, греп `The refusal band`) дословно: > «the invocation was turned down BEFORE it did any work of its own — nothing reached a provider, nothing > was spent, **and nothing this process would have written was written**». ⇒ **Потребитель контракта держит гарантию, которую производитель контракта ОТОЗВАЛ.** Цена названа в обоих файлах и она разрушительная: по `PD-196` интейк платформы действует по полосе РАЗРУШИТЕЛЬНО (удаляет загрузку пользователя), и движок прямо предупреждает — «A consumer keys a destructive action on a CLASS it knows, never on band membership». Платформенная копия учит противоположному. ⚠ **И это ровно то, что предсказал ратифицированный закон шва.** `17-seam-inbound-law.md` п.6: «Словарь движка ↔ словарь контракта — одна таблица, два направления, один файл… две копии разъедутся». Предсказание сбылось в ЧИТАЮЩЕМ направлении, которое п.6 считал безопасным. ⚠ **Вторая половина того же расхождения:** движок объявляет `exitBookIncomplete = 16` (`backend/cmd/tmctl/main.go`, греп `exitBookIncomplete`), у платформы КОНСТАНТЫ с этим номером нет вовсе. Здесь дизайн полосы сработал как задумано (незнакомый номер поглощается членством и читается как «refused»), и это положительная улика ЗА полосу — но перечень у потребителя неполон, и знает об этом только тот, кто сверил оба файла. ⚠ **Что это НЕ отменяет — и я проверила специально, потому что от этого зависит A2:** денежная половина обещания («nothing reached a provider, nothing was spent») цела в ОБЕИХ копиях. Запрет отказывать в A2 классом полосы стоит, и теперь он подпёрт формулировкой САМОГО ДВИЖКА, а не только платформы. **Диспозиция:** правка платформенного файла — ЧУЖАЯ ЗОНА, я её не трогаю. Уезжает пингом. ⚠ **И честная оговорка о его отношении к ЗАКОНУ: A15 — НЕ экземпляр закона.** Ст. 1 определяет подотчётный факт как ЗНАЧЕНИЕ, которое движок вычислил или выводит из своих строк; «два рукописных комментария о словаре разъехались» — не значение, не выводится ни из одной строки и не имеет канала (натяжка «экземпляр §2.4 на уровне словаря» снята). **A15 остаётся находкой первого класса и НЕ становится доводом за закон.** Что из него следует для закона: у Г1 обязана быть сверка с ЗЕРКАЛОМ чужого словаря (§7), иначе следующий разъезд будет так же тихим, — но это требование к гейту, а не подведение факта под статью. ### A13. Субстрат A0 сам может молча потерять строки `internal/store/requestlog.go` дословно: *«a telemetry-write failure is logged and never fails the translation»*, и `LogRequest` — *«insert, WARN on failure, never propagate»*. ⇒ Любая поверхность, построенная НА `request_log` (а срез A0 `ok=0 AND cost_usd>0` — именно такая), может занижать и **ничем этого не помечает**. По §2.3 такая поверхность обязана нести базис: сверку числа строк `request_log` против числа `checkpoints` за тот же прогон, и говорить «неполно», когда они разошлись. *(Замер: на `coldrun-v16` расхождения нет — 50 чекпоинтов, и все оплаченные вызовы в логе. Дыра латентная, а не наблюдённая. Помечено PLAUSIBLE честно.)* ### A14. `openEvents` — прогон, чей поток мёртв, платформе неотличим от прогона без потока `internal/pipeline/events.go` (греп `could not open the run-event journal`): журнал, который не удалось ОТКРЫТЬ, — ERROR в лог и прогон продолжается. Платформа при этом видит **отсутствие потока**, что по контракту читается как «процесс умер до первой строки». Факт «поток мёртв, прогон жив» движок ЗНАЕТ и на канал платформы не выводит; резинк через `status --json` есть, но он не отличает эти два случая. *(Диспозиция: назвать в законе как незакрытый экземпляр §2.1; правка — не этого пака.)* --- ## 7. ГЕЙТ — чем ловится ДВЕНАДЦАТЫЙ Промт: «из ~818 указателей доков по содержимому проверялись 89, и потому 34 уехали молча». Тот же исход ждёт этот закон без машины. Четыре гейта, **ранжированы по несущей способности**, и я честно говорю, какой из них ГЛАВНЫЙ. ### Г1 — НЕСУЩИЙ. Реестр раскрытия + рефлективный конформанс-тест Один Go-файл: таблица `disclosure` — на каждое ПОЛЕ каждого опубликованного документа (`StatusReport` · `ChapterPassport` · `StatusArtifacts` · `BookExport` · `BuildReport` · payload'ы `runevents`) одна запись: ``` {Doc: "status", Field: "config_drift", Consumers: {Operator, Polygon}, Basis: "config_drift_basis"} {Doc: "status", Field: "rebill_usd", Consumers: {Operator}, Basis: "rebill_basis"} {Doc: "status", Field: "done", Consumers: {Platform}, Basis: BasisTotal} ``` Тест ходит по структурам **РЕФЛЕКСИЕЙ** и валит сборку, если: * у поля нет записи (⇒ **новое поле нельзя добавить молча** — это и есть ловля двенадцатого); * запись называет `Basis:` поле, которого в документе нет; * запись объявляет потребителя `Platform`, а поля нет в аллоулисте платформы (сверка — по зеркальной константе, обновляемой при синке шва; кросс-зонная сверка автоматом невозможна, и это названо ограничением, а не сделано вид, что её нет). **Почему рефлексия, а не анализатор:** список полей опубликованного документа — это в точности набор json-тегов его типа, и рефлексия читает его без единой эвристики. ⛔ **ЧЕГО РЕФЛЕКСИЯ НЕ УМЕЕТ — названо, иначе гейт читается сильнее, чем он есть:** * **рефлексия перечисляет ПОЛЯ названного типа, но не ТИПЫ.** Список документов остаётся РУЧНЫМ, а «кто-то забыл дописать тип» — та же тихая дыра, ради которой гейт заводится (первый же перечень этого дока пропустил `PhaseProgress`, `WaveCounter` и `ManifestChapter`); * **payload'ы потока спрятаны за `Envelope.Data json.RawMessage`** — обход по `Envelope` не видит НИ ОДНОГО payload'а, а НОВЫЙ тип кадра (ровно туда садится рекомендация A5) невидим полностью; ⇒ **честная форма Г1: реестр перечисляет ТИПЫ руками, а рефлексия проверяет ПОЛЯ каждого.** Ручная половина закрывается отдельно и дёшево — компиляционным `var _ = []any{StatusReport{}, …}`, который краснеет при добавлении типа в пакет только если его туда впишут; полной защиты от «забыли тип» у Г1 НЕТ, и это ограничение, а не деталь. ### Г2 — «известно, но не опубликовано»: реестр КОЛОНОК схемы Та же таблица, вторая половина: каждая колонка движковой схемы получает либо `PublishedIn: [...]`, либо `Private: "<причина>"`. Тест сверяет таблицу с ЖИВОЙ схемой (`PRAGMA table_info` по `SchemaHead`) и краснеет на любой неучтённой колонке. Это тот гейт, который поймал бы блокер прогона: `chunk_status.first_flag_reason` приватна, и платформа из пятнадцати значений `flag_reason` видит одно. ### Г3 — «гард называет сравнённое поле»: табличный тест по ПОЛЯМ снапшота Для каждого поля payload'а `buildSnapshotID`, КОТОРОЕ ФИКСТУРА МОЖЕТ СДВИНУТЬ: сдвинуть ровно его → снять сообщение гарда → утверждать, что оно НАЗЫВАЕТ это поле. Это машинная форма §2.2. ⚠ **Оговорка, без которой гейт описан сильнее, чем он есть:** «~30 плоских осей» — неверно дважды. Payload — ДЕРЕВО (`contextSnap` · `segmentationSnap` · `coverageSnap` · `sanitizerSnap` · `banknoteSnap` · `repairSnap` · `[]stageSnap`), и заметная часть его листьев — КОДОВЫЕ КОНСТАНТЫ (`chunker_version` · `estimator_version` · `max_tokens_policy` · `classifier_version` · `style_check_version` · `render_format_version` · `embedded_version` · `pipeline_core`), которые фикстурой не двигаются вовсе — только правкой исходника, чего пак не делает. ⇒ **Г3 покрывает КОНФИГУРИРУЕМОЕ подмножество осей, а не все.** Для константных осей единственная честная форма — не тест, а требование к сообщению: гард, не сумевший назвать поле, обязан сказать «поле назвать не смог», а не подставлять причину. Это §2.2 в чистом виде и оно дешевле любого теста. ### Г4 — «необратимое до ошибки»: анализатор `archguard` Площадка уже есть (`internal/archguard`, три анализатора, `go vet -vettool` из `make battery`). Правило: в теле функции, где ЛЕКСИЧЕСКИ ПОСЛЕ вызова из множества «необратимых» (`SettleWithCheckpoint` · `commit` · `os.Remove` · `os.Rename`) стоит `return` с ненулевой ошибкой и НУЛЕВЫМ отчётом, — репорт. ⚠ **Ложные срабатывания будут**, поэтому предлагается с механизмом исключений по образцу существующего `exempt` (по импорт-пути + базовому имени) и **последним по очереди**: сначала Г1–Г3, он дороже и слабее. ### ⛔ ГЛАВНОЕ ОГРАНИЧЕНИЕ ГЕЙТА **Гейты покрывают НЕ ТЕ каналы, в которые закон маршрутизирует большинство корпуса** (сверка §5 против §7): шесть строк из десяти закон отправляет в канал, за которым не стоит НИ ОДИН из четырёх гейтов — A0 → секция `report`, A2 → строка об усечении, A3(а) → отчёт + WARN, A3(б) → строка сборки, A4 → текст гарда (stderr), A8 → репо-гейт (свой собственный). Г1/Г2 стерегут JSON-структуры и колонки БД, Г3 — одну ось одного сообщения. ⛔ **И ЧЕТВЁРТЫЙ ГЕЙТ НАЗЫВАЮ НЕПОСТРОЕННЫМ ВСЛУХ, потому что Ст. 4 этого требует буквально.** **Г4 (анализатор «необратимое до ошибки») НЕ ПОСТРОЕН** — он спроектирован и отложен, и §2.5 остаётся **НЕЗАГЕЙЧЕННОЙ статьёй**: сегодня её держит одна точечная посадка на `build` (A3а), а не машина, которая поймала бы СЛЕДУЮЩИЙ экземпляр в другом месте. Статус на 31.08: **Г1 не построен · Г2 не построен · Г3 построен точечно (A4, по снапшотной оси) · репо-гейт A8 построен · Г4 не построен.** ⇒ **машинная половина закона достаёт до четырёх строк из десяти — закон загейчен на 40%.** Это не довод против закона: статьи §2.1–§2.5 остаются ревью-вопросами и без машины, — но это ЧЕСТНАЯ цена, и приёмка обязана её знать. Дозакрыть прозаические каналы можно только гейтом на ТЕКСТ (посадка на сообщение с проверкой красноты при подмене строки, D39.171), и это отдельная работа, которую я НЕ проектировала. ### Чего гейт НЕ ловит — говорю вслух * Факт, который движок вычисляет и **не пишет ни в схему, ни в документ** (A5 до правки: `VolumeStop` живёт в памяти). Г1/Г2 его не видят. Единственная защита — ст. 1 как ревью-вопрос при появлении новой структуры результата. * **Ложь в ПРОЗЕ** вне снапшот-гарда: Г3 закрывает только снапшотную ось. * Расхождение с ЖИВОЙ платформой: кросс-зонная сверка требует общего артефакта, которого шов намеренно не имеет (D39.81/85). Г1 сверяет с ЗЕРКАЛОМ, и зеркало может протухнуть — это честная граница, а не закрытая дыра. --- ## 8. Что закон ТРЕБОВАЛ на ратификацию — и что с этим стало **Все три пункта ратифицированы 31.08 (D39.181) и построены** — перечень в шапке файла, разбор каждого экземпляра — `MONEY_HONESTY_REPORT.md` §9. ⚠ **A10 оркестратор НЕ отнёс к классу «новое поле»**: это исправление ДЕРИВАЦИИ существующей фигуры, что закон и предписывает. Условие — «ни одна фигура, которую читает платформа, не смеет сменить смысл» — выполнено и предъявлено тестом (`committed_usd` остаётся `SUM(checkpoints)` до микро-доллара). ## 9. Что я в этом дизайне НЕ проверила * **Не проверяла, согласится ли платформа** декодировать предложенные поля — чужая зона, вопрос уехал пингом вместе с дизайном. * Реестр Г1 оценён по составу документов, но **не написан**; цена «одна запись на поле» — оценка, а не замер.