textmachine/backend/docs/DISCLOSURE_LAW_DESIGN.md

674 lines
73 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Закон раскрытия — дизайн на ратификацию (фаза 1 пака «ДЕНЬГИ И ЧЕСТНОСТЬ ВЫДАЧИ»)
> **СТАТУС: РАТИФИЦИРОВАН ВЛАДЕЛЬЦЕМ 31.08 — нота D39.181.** Норма зоны. Ратифицированы три вещи: сам
> закон · поля леджера доставки в кадре `finished` (A5) · `config_drift_basis` в `status --json` (A7/A12).
> **Все десять экземпляров корпуса ПРИМЕНЕНЫ** — разбор по пунктам в `MONEY_HONESTY_REPORT.md` §9.
> ⚠ Оговорка к Ст. 3 (дешевизна полосы 1019) дописана ПОСЛЕ ратификации по находке оркестратора и стоит
> ниже; она сужает полномочие, а не расширяет его.
> ⚠ Честная цена, названная при ратификации: **закон загейчен примерно на 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:321`=`the UNSIGNED BANK count is unknown, not zero`,
`internal/pipeline/status.go:839`=`the unsigned-term count is unknown, not zero`,
`internal/pipeline/status.go:922`=`reported as unknown, not as none`,
`internal/pipeline/bookbuild.go:231`=`(reported as unknown, not as none)`.
2. **Фигура едет С БАЗИСОМ**`RebillBasis` (`internal/pipeline/status.go:293`=`RebillBasis string`),
четыре значения `pending|stored|none|failed`
(`internal/pipeline/bankmaterialize.go:364-367`), и её докстринг формулирует ровно закон:
*«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 + ПОЛОСА 1019; **неизвестный код → `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`, пересекающее шов · код выхода ВНЕ полосы (05) | ратификация ВЛАДЕЛЬЦА |
| **НОВЫЙ КЛАСС ОТКАЗА ВНУТРИ полосы 1019** | новая константа в `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). Полоса спроектирована расти;
заморожена верхняя шестёрка 05. ⚠ Для 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` о полосе 1019 — *«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 оценён по составу документов, но **не написан**; цена «одна запись на поле» — оценка, а не
замер.