Compare commits
103 commits
c05a3537a5
...
360bdc165b
| Author | SHA1 | Date | |
|---|---|---|---|
| 360bdc165b | |||
| 8a9a9f4a62 | |||
| f08325ea59 | |||
| e69902ab70 | |||
| 878cac58d5 | |||
| 6457d778ef | |||
| b5cdd3dc11 | |||
| d8f723fa2b | |||
| 8d6166392b | |||
| 3dd8994f15 | |||
| 22e079efbd | |||
| f606e511a3 | |||
| 584c94a100 | |||
| 0632a3086c | |||
| 022d953a03 | |||
| 3eb822312e | |||
| 4860d4a332 | |||
| 35d7fa3375 | |||
| 42b793dd50 | |||
| cc0abea948 | |||
| 5a347b5b7e | |||
| b32ee9a860 | |||
| c0357df42a | |||
| 78c426bf07 | |||
| fd9b244020 | |||
| bb1f6bf817 | |||
| 081822af7e | |||
| e4211d4d80 | |||
| d16591c976 | |||
| cb36fb5cfb | |||
| e95d03271e | |||
| 7203026561 | |||
| 2a89b52057 | |||
| bf062a98a5 | |||
| 49c109314d | |||
| 3524e47d4b | |||
| 3435bd6eee | |||
| ff07b87288 | |||
| def4278f67 | |||
| a425fbf054 | |||
| 44faafcb07 | |||
| 3cabe69412 | |||
| e92d411cca | |||
| 201363b2a7 | |||
| 7f4cf41864 | |||
| d46d03d4c5 | |||
| 89b1313df1 | |||
| 8f69794444 | |||
| 41861f9181 | |||
| 3327ac80a4 | |||
| 1a4b32447e | |||
| be45500475 | |||
| cf8b3ec327 | |||
| 2ed1c0b394 | |||
| 93f098c815 | |||
| c206067623 | |||
| f05795ed7b | |||
| 1cae0ac494 | |||
| 33b896b1ac | |||
| 32a60509fe | |||
| 32317b9610 | |||
| b888320fc1 | |||
| 0f0d3c8062 | |||
| 518121b675 | |||
| abdfd04d7b | |||
| f1e9226b97 | |||
| 7138a4a24f | |||
| 0eb8511a8c | |||
| 6f08e59784 | |||
| edf7c6cc46 | |||
| 3e40c36a03 | |||
| 6ceb133b84 | |||
| 5576bff608 | |||
| e888d0f3de | |||
| 5ac3f61161 | |||
| fde85307d4 | |||
| 5f4c6d8754 | |||
| a1b4151956 | |||
| b206e1cef2 | |||
| abb0f0d5b7 | |||
| 5d54824f39 | |||
| a049fa0b56 | |||
| ab0ab998a0 | |||
| 5dcc351b52 | |||
| 99daa600b8 | |||
| 9845098f67 | |||
| 6069e52aac | |||
| c71692efdc | |||
| a5ef47cd11 | |||
| 98624c7cfd | |||
| 32bbadbc96 | |||
| bb6f3f4841 | |||
| 05cdfaafe7 | |||
| cf4269a8c2 | |||
| b856105c30 | |||
| 15e263dda8 | |||
| a195eda743 | |||
| cbbb5dfc01 | |||
| 98d2d4ad18 | |||
| 2812dd67fa | |||
| 898c8401dc | |||
| d0fce7d793 | |||
| db68476e12 |
56 changed files with 3880 additions and 280 deletions
20
CLAUDE.md
20
CLAUDE.md
|
|
@ -70,8 +70,28 @@ Go-бэкенд издательского художественного пер
|
|||
- **Общность движка:** Go-логика НЕ ветвится по паре/книге; пара-данные → `internal/lang`+`configs/langpacks/`, книго-каноны → сид/brief (данные); книжный термин в общем пар-слое = утечка. Тест-данные пары легитимны. Норматив (только больные места): `docs/architecture/12-go-style-notes.md`. Ревью-вопрос по умолчанию — §Цели п.2.
|
||||
- **При компакции/сжатии контекста ВСЕГДА сохранять:** список изменённых файлов · незакрытые находки ревью и их диспозиции · обязательства и открытые вопросы сессии · команды тестов (D39.121).
|
||||
- **Тесты и гейты не подгонять под зелень:** править или удалять тест/голден/гейт, чтобы он прошёл, — НЕДОПУСТИМО; несогласие с тестом — вопрос оркестратору пингом, не правка (D39.121). ⚠ **Запрет — про МОТИВ (D39.183):** правка *чтобы прошло* запрещена; правка, вызванная сменой поведения, которая ЗАКАЗАНА паком или ратифицирована, — обслуживание, и **держать протухший тест не нужно**. ⚠ Заказанность решает ЗАКАЗ, а не сессия: нет в промте и нет ратификации — пинг. Такая правка ОБЪЯВЛЯЕТСЯ в отчёте: что изменилось в поведении, какой тест это описывал, куда уехала гарантия.
|
||||
- ⛔ **ОТРИЦАТЕЛЬНЫЙ ЗАМЕР ОБЯЗАН ДОКАЗАТЬ, ЧТО СПРОСИЛ СУЩЕСТВУЮЩЕЕ.** «Ноль строк» и «нет такой таблицы/файла/поля»
|
||||
в выводе НЕРАЗЛИЧИМЫ и одинаково выглядят успехом — а в деньгах это разница между «не потратили» и «не смотрели».
|
||||
⇒ рядом с нулём ПЕЧАТАЕТСЯ КОНТРОЛЬНАЯ величина, доказывающая, что вопрос задан существующему предмету
|
||||
(«БД без этой таблицы: 0», «файлов прибор прочёл: 67»). ⛔ **Именно печатается, а не упоминается:** фраза
|
||||
«проверено с контрольной строкой» — это утверждение О проверке, и читатель отчёта видит слово, а не число.
|
||||
Иначе норма ловит замер и НЕ ловит отчёт о нём, то есть переносит дефект на шаг дальше — туда, где смена
|
||||
06.09 ловила его пять раз. Поправку принёс автор исходного замера, нарушив её в том же письме, где предложил. Замер 06.09: сессия
|
||||
весь день писала `$0` ПО РАССУЖДЕНИЮ, пошла мерить — и первый же запрос дал `COUNT(*) = 0` по таблице, которой в
|
||||
схеме НЕТ. Пере-замер по настоящим таблицам с контрольной строкой дал честный ноль. **Прибор ответил на свой
|
||||
вопрос, а прочитан был как ответ на твой** — тот же класс, что `D39.202`, только в проверке ДЕНЕГ.
|
||||
- **Мандат самопроверки в промтах сессий (обязателен, решение владельца):** промты полигон-сессий — и вообще любых пишущих код / запросы к моделям — ДОЛЖНЫ явно требовать ревью ИСПОЛНЕНИЕМ: своего кода + сформированных запросов к моделям + полученных результатов. Сессии регулярно ошибаются и багуют, и это искажает результат; **самоотчёт «проверено» без исполнения регулярно оказывается ложным и стоит денег**. Бэкенд-промты — явный бэкенд-ревью после кода (обычно отрабатывает по опыту, но требовать явно). Пост-хок адверсариальная верификация оркестратора при лендинге — второй рубеж, НЕ замена самопроверки.
|
||||
⚠ **Исполнения мало: промт заказывает адверсариальный проход сессии по СВОЕЙ готовой работе; глубину и веер сессия выбирает под предмет, промт даёт направление — что в этом паке уязвимо.**
|
||||
⛔ **МУТАЦИЯ ЗАСЧИТЫВАЕТСЯ ПО ТЕКСТУ СООБЩЕНИЯ, А НЕ ПО ФАКТУ КРАСНОТЫ.** Замер 06.09: мутация
|
||||
«подметать все логи без разбора» покраснила гейт — сообщением «рецепт НИКОГДА не удаляет старый лог», то есть
|
||||
о рецепте, который первым же действием удалял их все. Причина: `make` клеит префикс тишины к первой команде,
|
||||
первый токен стал `@rm`, и детектор удаления его не увидел. **Правый вердикт по неправой причине — дыра, а не
|
||||
поимка**, и от настоящей поимки он отличается ровно тем, прочёл ли кто-нибудь ТЕКСТ, а не цвет. Нашла зона,
|
||||
потому что формулировка не сошлась с тем, что она мутировала.
|
||||
⭐ **ПРИЁМ, СТОЯЩИЙ ОДНОЙ СТРОКИ И НИ ОДНОГО АГЕНТА (замер 06.09):** когда приходит письмо «всё закрыто»,
|
||||
перестань проверять ЧУЖИЕ правки и иди перечитывать СВОИ утверждения о том, что закрыто. Из шести случаев
|
||||
класса «прибор ответил на свой вопрос» за смену два нашлись именно так, и оба — автором утверждения, а не
|
||||
ревьюером. Письмо «всё закрыто» — сигнал не расслабиться, а сменить предмет проверки на собственный.
|
||||
⚠ **Fable 5** — обычно хватает одного-двух агентов, но это РЕКОМЕНДАЦИЯ, не потолок. Модель задавай агенту ЯВНО и знай, сколько их у тебя работает.
|
||||
- **Git-координация мультисессий:**
|
||||
- **Коммитит только оркестратор.** Сессии зон — бэкенд, полигон, фронт, платформа — не коммитят: своё дерево готовят и передают на лендинг (исключение: пре-рег фризы полигона). Чужую зону не трогает НИКТО.
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@
|
|||
|
||||
| | команда | EXIT | пакетов | FAIL | линтер | скипы |
|
||||
|---|---|---|---|---|---|---|
|
||||
| до правок | `make battery` | 0 | 19 | 0 | 0 issues | **3** |
|
||||
| после | `make battery` | 0 | 19 | 0 | 0 issues | **3** |
|
||||
| до правок | `make battery` (одной командой) | 0 | 19 | 0 | 0 issues | **3** |
|
||||
| после | ⚠ ПО СТАДИЯМ — см. §11 | 0 | 19 | 0 | 0 issues | **3** |
|
||||
|
||||
⚠ **Строка «после» — НЕ `make battery`.** Одной командой она не прошла: система дважды убила её по нехватке
|
||||
памяти. Прогон сделан по стадиям, команды перечислены в §11. Первая редакция этой таблицы писала `make
|
||||
battery` в обеих строках и противоречила §11 того же отчёта.
|
||||
|
||||
Скипы поимённо и в обоих прогонах те же: `TestMinerFullBookParity` (корпусный, снимается `battery-stand`),
|
||||
`TestHelperEventsRun`, `TestHelperKillLoop` (хелперы подпроцессов). **Новых скипов не появилось** — это
|
||||
|
|
@ -21,7 +25,7 @@
|
|||
|
||||
### Каталог мутаций
|
||||
|
||||
201 → **205 записей**. Шесть прогнаны исполнением на КОПИИ дерева (`~/tm-mut-0509`, `GOFLAGS=-buildvcs=false`),
|
||||
201 → **211 записей** (первая редакция говорила 205 — это было состояние ПЕРВОГО круга, до шести записей дофикса). Шесть прогнаны исполнением на КОПИИ дерева (`~/tm-mut-0509`, `GOFLAGS=-buildvcs=false`),
|
||||
**6 RED, 0 неожиданных исходов**:
|
||||
|
||||
| запись | покрасневший тест |
|
||||
|
|
@ -52,7 +56,8 @@
|
|||
|
||||
### Счета формата
|
||||
|
||||
`form feeds ignored`, `documents attached`, `documents excluded`, `toc unresolved`, `targets collapsed` живут в `chunk.Document`,
|
||||
`form feeds ignored`, `documents attached`, `documents excluded`, `toc unresolved`, `targets collapsed`,
|
||||
`service roles inside documents` — ШЕСТЬ счётов — живут в `chunk.Document`,
|
||||
уезжают в WARN (`events.go`, `ingestSource`) и **НЕ на провод** — потребителя у них сегодня нет.
|
||||
|
||||
---
|
||||
|
|
@ -197,13 +202,13 @@ unit-id главы 1 `c3aa4cbbb53de134:d4fb75e2:0` → `c3aa4cbbb53de134:1b30b1d
|
|||
|
||||
## 6. ЧТО НЕ ПРОВЕРЕНО И ПОЧЕМУ
|
||||
|
||||
- **Полный каталог (205 записей) не гонялся.** Прогнаны 6 записей, целящих в изменённое поведение. Причина —
|
||||
- **Полный каталог (211 записей) не гонялся.** Прогнаны 14 записей, целящих в изменённое поведение. Причина —
|
||||
цена: 124 записи гоняют `./internal/pipeline/`, чей набор идёт ~145 с, то есть полный прогон — часы при
|
||||
загруженном хосте. Механически проверено другое и дешёвое: **у всех 205 записей цель ЖИВА** (греп `find`
|
||||
загруженном хосте. Механически проверено другое и дешёвое: **у всех 211 записей цель ЖИВА** (греп `find`
|
||||
по дереву), то есть протухших целей после пака не осталось.
|
||||
- **EPUB владельца (Кристофф/en, isekai/ja) не прогонялись через новый путь.** §3 выводит их из гейта, а
|
||||
§2.1 п.3 требует ОРАКУЛА, а не замера; оракул построен кодом (`TestEPUBNavOracleReproducesTheTxtCut`).
|
||||
Замер их нового разреза числом — не сделан.
|
||||
- ⚠ **Эта строка УСТАРЕЛА и оставлена как след:** на момент первой редакции EPUB владельца через новый путь
|
||||
не прогонялись. Они прогнаны позже — §8 и §10 несут их числа. Строку не удаляю, потому что она показывает,
|
||||
в каком порядке появлялось знание.
|
||||
- **Платная книга платформы** не трогалась — чужая зона.
|
||||
- **Сквозного прогона не-CJK нет** — промтов пары для en/ja не существует (§5), проверка ограничена
|
||||
уровнем ingest/manifest, $0.
|
||||
|
|
@ -434,3 +439,88 @@ go test ./... -count=1 -v → EXIT 0, o
|
|||
⚠ **Это второй раз за смену, когда правку ловил пере-замер, а не тест.** Первый — Д-2 (не пере-мерил книгу
|
||||
владельца после починки). Норма работает ровно потому, что она про ЭКЗЕМПЛЯР, а не про мнение.
|
||||
|
||||
---
|
||||
|
||||
## 12. ВЫЧИТКА ПОСЛЕ ЛЕНДИНГА — ЧТО НАШЛОСЬ В СВОЁМ ЖЕ КОДЕ
|
||||
|
||||
Пак был уже заланден (`2f65d1d`), когда владелец спросил, всё ли проверено. Простое чтение собственного
|
||||
дифа глазами — то, чего я за весь пак ни разу не сделал подряд, — дало находки за минуты и без единого агента.
|
||||
|
||||
### Дефекты кода (починены)
|
||||
|
||||
| # | что | почему это дефект |
|
||||
|---|---|---|
|
||||
| В-1 | **метка `nav` не триммилась**, а её сёстры триммят | один заголовок давал `"\n Chapter One\n "` через nav и `"Chapter One"` через NCX — два ответа на один вопрос, решаемые тем, какую таблицу собрал издатель. Едет на провод в `title_raw`. Фикстуры пака не ловили: они пишут метку без отступов |
|
||||
| В-2 | **поиск заголовка в манифесте без нижней границы** | сосед шестью строками выше пишет `k >= 0 && k < len(ids)` и деградирует с WARN, «если плотная нумерация однажды перестанет держаться». Моя строка в том же случае даёт панику индекса на платном прогоне |
|
||||
| В-3 | `ncxPath` — единственный резолвер href, не раскодирующий проценты | ровно та асимметрия, которую пак свёл в `hrefTarget`; я свёл два резолвера и не заметил третий |
|
||||
|
||||
Пин на В-1 — `TestNavLabelIsTrimmedLikeItsSiblings`, предъявлен посадкой (снять trim → красный).
|
||||
|
||||
### Протухшие носители (девять, починены)
|
||||
|
||||
`resolveTOC` обещал счёт нелинейных целей, которого я сам же лишил его при M-5 · `epubTOC.unresolved` ссылался
|
||||
на `DocumentsExcluded`, переименованный в `Excluded` · `isChapterHeader` нёс имя `isCJKChapterHeader` ·
|
||||
`Chunk.Heading` — «detected via the pair's lang.HeadingRule» · `stripHeading` в `SplitChunksWithChapters` —
|
||||
«A nil rule» · шапка `ingest.go` и `chapterSep` — «form feed семантически граница главы» и «epub несёт
|
||||
настоящую структуру глав в spine», то есть ровно те два убеждения, которые пак опровергает · `detectChapterUnit`
|
||||
— четыре юнита и не тот источник · `buildManifest` — параметр `chapterTexts`, которого нет · `bookfile/model.go`
|
||||
— «титульный документ был бы прочитан как отдельная глава» · комментарий сторожа в `ingestnotes_test.go`
|
||||
обещал «любой тип», а `switch` покрывает два.
|
||||
|
||||
⛔ **Класс один и тот же: утверждение, верное на момент написания и не пере-прочитанное после правки.** Он же
|
||||
дал шесть находок приёмки и четыре моих. Дешевле всего он ловится не тестом, а чтением подряд.
|
||||
|
||||
### Ошибки в ЭТОМ отчёте (исправлены)
|
||||
|
||||
Каталог 205 → **211** (число было от первого круга) · таблица §1 писала `make battery` в обеих строках и
|
||||
противоречила §11 того же отчёта · «Счета формата» перечисляли пять, их шесть · строка §6 «EPUB владельца не
|
||||
прогонялись» осталась после того, как §8 и §10 их прогнали.
|
||||
|
||||
### Перепись гейта была неверной — и я поймал её собственной нормой дня
|
||||
|
||||
Я считал конфиги **по ИМЕНИ файла** (`book.yaml`, `*.book.yaml`) и получил 31 конфиг / 8 исходников.
|
||||
Счёт **по СОДЕРЖИМОМУ** (файл несёт `book_id` и `source_file`) даёт **39 конфигов и 12 различных
|
||||
исходников**. Четыре источника семьи 蛊真人 под `books/gu-zhenren/exp15/` гейт не покрывал: их конфиги
|
||||
называются `exp15-*.yaml`, и фильтр по имени их не видел.
|
||||
|
||||
**Это четвёртый за смену случай класса «имя против содержимого» — и первый, где на него наступил я сам,
|
||||
уже объявив его нормой.**
|
||||
|
||||
Четыре пропущенных источника пинованы «до» и «после» отдельно:
|
||||
|
||||
| исходник | глав | structure до → после | разрез |
|
||||
|---|---|---|---|
|
||||
| `S2prime.txt` | 1 | none → none | байт-в-байт |
|
||||
| `S2prime_big_flat.txt` | 1 | none → none | байт-в-байт |
|
||||
| `S2prime_big_blanklines.txt` | 1 | none → none | байт-в-байт |
|
||||
| `smoke.txt` | 1 | none → none | байт-в-байт |
|
||||
|
||||
Пробел последствий не имел, но он БЫЛ, и закрыт замером, а не доводом.
|
||||
|
||||
### Два заказа §2.1/§2.5, которых я не сделал и не назвал
|
||||
|
||||
⛔ **Полная 蛊真人 (2283 главы) через новый путь не прогонялась.** §2.1 велит: «Полную книгу гоняешь
|
||||
ОТДЕЛЬНО, СВОИМ конфигом в своём каталоге; её результат идёт числом в отчёт». Я записал в §6 «у неё нет
|
||||
конфига» — но отсутствие конфига и есть то, что заказ приказывал устранить. **Это не «не проверено», это
|
||||
не сделано.**
|
||||
|
||||
⛔ **Машинная карта сдвигов не была построена и не была названа среди непроверенного.** §2.5: «Прозой
|
||||
нельзя: на прозе не построить мигратор потом». Отчёт давал прозаическую таблицу восьми строк.
|
||||
|
||||
### ОБА ЗАКАЗА ИСПОЛНЕНЫ ПОСЛЕ ВЫЧИТКИ
|
||||
|
||||
**Полная 蛊真人 прогнана своим конфигом** (`~/tm-pins-0509/full/`, `tmctl manifest`, EXIT 0, 10 с, $0):
|
||||
**2283 главы**, `structure = detected`, 4276 юнитов, 5071 чанк, `title_raw` заполнен у **2283 из 2283**.
|
||||
Счёт глав сошёлся с независимым замером шапок шага 0 (2283 строки `第N节`) — два прибора, один ответ.
|
||||
|
||||
**Машинная карта сдвигов построена** — `backend/docs/CHAPTER_STRUCTURE_SHIFTMAP.json`, генератор рядом
|
||||
(`chapter_structure_shiftmap.py`). Set-diff по id и номеру, `same/moved/gone/new`, по **тринадцати**
|
||||
источникам: восемь пинов гейта, четыре пропущенных переписью, и полная книга.
|
||||
|
||||
| | глав | same | moved | gone | new |
|
||||
|---|---|---|---|---|---|
|
||||
| **все 13 источников** | **2355** | **2355** | **0** | **0** | **0** |
|
||||
|
||||
⇒ ветка (а) гейта §3 предъявлена не таблицей глазами, а машинным набором, и на нём строится мигратор,
|
||||
если он понадобится. Это и было содержанием заказа.
|
||||
|
||||
|
|
|
|||
275
backend/docs/CHAPTER_STRUCTURE_SHIFTMAP.json
Normal file
275
backend/docs/CHAPTER_STRUCTURE_SHIFTMAP.json
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
[
|
||||
{
|
||||
"source": "ch1",
|
||||
"before": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "1199f7ef6aff4863dbd6bcdacf7f9313ea928d79f5f216ab5687d5f2c9022bf4"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "37db0ea49c3dcae65c43a8ab22e5c054c8f252c6cac509cf66e69e63169bcf41"
|
||||
},
|
||||
"same": 1,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "ch1-2",
|
||||
"before": {
|
||||
"chapters": 2,
|
||||
"structure": "detected",
|
||||
"key": "f368bcf0e25bdff52e6f14a751ccc13e151b2bc475d4931cec30c0cd7f255be2"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 2,
|
||||
"structure": "detected",
|
||||
"key": "cdb5056e9a7440b2ba1fa6bcead16b4370d72a6ce0dc231e76e02b441c1e19f1"
|
||||
},
|
||||
"same": 2,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "ch1-5",
|
||||
"before": {
|
||||
"chapters": 5,
|
||||
"structure": "detected",
|
||||
"key": "1f2be0a61722c80cbf15e7539e5cd4826ee409aefb95ff9795d2a16137c7f2de"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 5,
|
||||
"structure": "detected",
|
||||
"key": "748f069774e72cbdeb3e3e344a05e392e60205161db819c6149d52d948445fa4"
|
||||
},
|
||||
"same": 5,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "ch4-5",
|
||||
"before": {
|
||||
"chapters": 2,
|
||||
"structure": "detected",
|
||||
"key": "be3010c163605ac444d5486e49599e4f0af19760858d9787c333d23ecfb9e360"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 2,
|
||||
"structure": "detected",
|
||||
"key": "b15aa9fa7837e373ff0893774978f9e423d863aa7119425ca8fbb1116a1568f0"
|
||||
},
|
||||
"same": 2,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "ch1-10",
|
||||
"before": {
|
||||
"chapters": 10,
|
||||
"structure": "detected",
|
||||
"key": "1e62a68d93f73534c27a1c67b1e98b813d5a5916986159259f307bd635e9bb03"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 10,
|
||||
"structure": "detected",
|
||||
"key": "01a00e3669302bc14accd9d05a8f6a0155398f817ac1d1419d6dbc92aa9419a1"
|
||||
},
|
||||
"same": 10,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "s11-16",
|
||||
"before": {
|
||||
"chapters": 6,
|
||||
"structure": "detected",
|
||||
"key": "b806ba144cecc402e4d130a7b42d4f04c9d63ca4d114e9772ef95ab7d8d4b577"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 6,
|
||||
"structure": "detected",
|
||||
"key": "c1148887ca4c977339969a6fc99f740d7b5a8674f8a602b34572dc1dbc71d4a8"
|
||||
},
|
||||
"same": 6,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "slice25",
|
||||
"before": {
|
||||
"chapters": 25,
|
||||
"structure": "detected",
|
||||
"key": "084b129293ba410f7245a57ee99b84a2bb4f3c4bb32aa6a24ff0a52c4e3fc246"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 25,
|
||||
"structure": "detected",
|
||||
"key": "6dbbbe3490343526781f9577c852426802b2b1b1962526e71bc07c28d8f4643b"
|
||||
},
|
||||
"same": 25,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "ch26-42",
|
||||
"before": {
|
||||
"chapters": 17,
|
||||
"structure": "detected",
|
||||
"key": "8f7aab0de5e785f0e25b89ebb2730c55c5fdc435cbcfb234a91c54608a42107d"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 17,
|
||||
"structure": "detected",
|
||||
"key": "8a936b25aa719179557a3c4728e1f1cfa5d4ffa0888a03d5407de9fdb74ef837"
|
||||
},
|
||||
"same": 17,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "S2prime",
|
||||
"before": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "511c1f6e9abe2c0a1818c0bfc5ba14d970ae18869195b4f3d0acea4fd32fe7a9"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "b3ce41a67e170d3cf1c912a197bf279a1ae22515fc9665b335d6a2c97596ee24"
|
||||
},
|
||||
"same": 1,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "S2prime-flat",
|
||||
"before": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "0e7c8df7be6edbf0c0c7cd4d511d9ff0bb6a8f996a051cd214dc2378bbfc34e1"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "084466f455aff0cbd32533d7f4d192e1a7e1243706dc3a0469190b0c4516d703"
|
||||
},
|
||||
"same": 1,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "S2prime-blank",
|
||||
"before": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "c2f95f265b7772fe2834230e8e581119f03a00c8b02a460afc54e05b970f8b04"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "6c972cea05026d68a65d2b527f04bbbea8afcfe1d28bea933fe5a4b5b9f96626"
|
||||
},
|
||||
"same": 1,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "smoke",
|
||||
"before": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "a071430a17b96f7995bd54855c76fcf55d97ab082915351ec7ede748342d798b"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 1,
|
||||
"structure": "none",
|
||||
"key": "69131330d13d6d5f024eaca800c748466ed6a02e5240c0aeee081f274d601af2"
|
||||
},
|
||||
"same": 1,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
},
|
||||
{
|
||||
"source": "guzhenren-full-2283",
|
||||
"before": {
|
||||
"chapters": 2283,
|
||||
"structure": "detected",
|
||||
"key": "28fa8b38eda95e94f68c640901152350c66041dea7fd898e1710f02857f0c4c5"
|
||||
},
|
||||
"after": {
|
||||
"chapters": 2283,
|
||||
"structure": "detected",
|
||||
"key": "d1a249ec0b8a3dc7ab59d070409659bc520b77b496e1cb73b2cd5e295dd7a68c"
|
||||
},
|
||||
"same": 2283,
|
||||
"moved": 0,
|
||||
"gone": 0,
|
||||
"new": 0,
|
||||
"moved_detail": [],
|
||||
"gone_ids": [],
|
||||
"new_ids": [],
|
||||
"cut_unchanged": true
|
||||
}
|
||||
]
|
||||
29
backend/docs/chapter_structure_shiftmap.py
Normal file
29
backend/docs/chapter_structure_shiftmap.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Машинная карта сдвигов, заказанная §2.5: set-diff по id и номеру, same/moved/gone/new."""
|
||||
import json, sys
|
||||
|
||||
def load(p):
|
||||
m = json.load(open(p, encoding="utf-8"))
|
||||
return {c["id"]: c["number"] for c in m["chapters"]}, m
|
||||
|
||||
def diff(before_path, after_path, source):
|
||||
a, ma = load(before_path)
|
||||
b, mb = load(after_path)
|
||||
same = sorted(i for i in a if i in b and a[i] == b[i])
|
||||
moved = sorted(i for i in a if i in b and a[i] != b[i])
|
||||
gone = sorted(i for i in a if i not in b)
|
||||
new = sorted(i for i in b if i not in a)
|
||||
return {
|
||||
"source": source,
|
||||
"before": {"chapters": len(a), "structure": ma.get("structure"), "key": ma.get("key")},
|
||||
"after": {"chapters": len(b), "structure": mb.get("structure"), "key": mb.get("key")},
|
||||
"same": len(same), "moved": len(moved), "gone": len(gone), "new": len(new),
|
||||
"moved_detail": [{"id": i, "from": a[i], "to": b[i]} for i in moved][:200],
|
||||
"gone_ids": gone[:200], "new_ids": new[:200],
|
||||
"cut_unchanged": not moved and not gone and not new,
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = [diff(*t) for t in (sys.argv[1:][i:i+3] for i in range(0, len(sys.argv)-1, 3))]
|
||||
json.dump(out, sys.stdout, ensure_ascii=False, indent=1)
|
||||
print()
|
||||
|
|
@ -56,7 +56,7 @@ type Book struct {
|
|||
Description string
|
||||
// Notice are the book-level paragraphs a reader sees on OPENING the book — before the first chapter's
|
||||
// title, inside the first spine document (a separate title document would be read by ingest as a
|
||||
// chapter of its own), and at the head of the text file. Empty for a book with nothing to declare.
|
||||
// chapter of its own — before the reader learned to fold an uncovered leading document into chapter one), and at the head of the text file. Empty for a book with nothing to declare.
|
||||
Notice []string
|
||||
// Chapters in reading order; at least one (an EPUB with an empty spine is not an EPUB).
|
||||
Chapters []Chapter
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ type Chunk struct {
|
|||
// ("degradation is not silent") rather than silently oversized.
|
||||
OversizedSentence bool
|
||||
// Heading is the deterministic chapter title, non-empty ONLY on a chapter's FIRST chunk (ChunkIdx 0)
|
||||
// when the chapter opened with a source structural header (detected via the pair's lang.HeadingRule)
|
||||
// AND a rule is configured. The chunker STRIPS the source marker from ch.Text (so the model never
|
||||
// when the chapter opened with a source structural header (RECOGNISED by the source language's
|
||||
// lang.SourceStructure) AND the pair supplies a template. The chunker STRIPS the source marker from ch.Text (so the model never
|
||||
// renders its own «Раздел 2»/«Первая глава»/an orphaned « :») and renders this from the rule's template
|
||||
// («Глава N»); the read-models PREPEND it to the chapter's first output unit. "" for every non-first
|
||||
// chunk, every chapter without a header, and every book without a heading rule (in which case the source
|
||||
|
|
@ -128,7 +128,7 @@ func SplitChunksWithChapters(chapters []string, seg SegBudget, rule *lang.Chapte
|
|||
editUnitID := 0
|
||||
for idx, ingested := range chapters {
|
||||
// Title policy (pack-13): detect a leading structural header, render it deterministically and
|
||||
// STRIP its marker from the text the model sees. A nil rule (no pack / no heading.txt) or a chapter
|
||||
// STRIP its marker from the text the model sees. An absent TEMPLATE (no pack / no heading.txt) or a chapter
|
||||
// with no header is a NO-OP — headingText is "" and chapText is unchanged, so a book that does not
|
||||
// opt in produces byte-identical chunks. Runs BEFORE splitParagraphs so the stripped subtitle is
|
||||
// re-paragraphed normally.
|
||||
|
|
@ -202,7 +202,7 @@ func ApplyHeading(heading, finalText string) string {
|
|||
|
||||
// matchHeaderLine reports whether a single line is a structural header under the rule and returns the
|
||||
// parsed section number + the trimmed subtitle after the marker. Shape: <marker><numeral-run><unit-rune>,
|
||||
// then EITHER end-of-line OR a SEPARATOR (not a content glyph) — mirroring ingest.isCJKChapterHeader's
|
||||
// then EITHER end-of-line OR a SEPARATOR (not a content glyph) — mirroring ingest.isChapterHeader's
|
||||
// precision guard so «第一回见面» (回 a measure word glued to the content 见) is NOT a header, while
|
||||
// «第一节:…» / «第1章 …» is. Leading whitespace is tolerated; the subtitle is the remainder with any
|
||||
// leading separators (:: 、,.。- — ·) trimmed. Deterministic.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ type Chapter struct {
|
|||
// Raw writes Body verbatim instead of wrapping it in the xhtml skeleton, for the nav document and
|
||||
// anything else that must control its own markup.
|
||||
Raw bool
|
||||
// Declared writes the manifest item but NOT the file, for a book that promises a document it does not
|
||||
// carry — the shape a scraped EPUB arrives in.
|
||||
Declared bool
|
||||
}
|
||||
|
||||
// SpineRef is one spine entry: the manifest id and its `linear` attribute ("" → linear, i.e. absent).
|
||||
|
|
@ -51,6 +54,8 @@ type Nav struct {
|
|||
// InSpine puts the navigation document in the spine, which real books do and which is exactly the case
|
||||
// the service-page exclusion has to handle.
|
||||
InSpine bool
|
||||
// Missing declares the navigation document in the manifest and omits its bytes.
|
||||
Missing bool
|
||||
}
|
||||
|
||||
// GuideRef is one EPUB 2 `<guide><reference>`: how a book with no navigation document declares that a
|
||||
|
|
@ -64,6 +69,10 @@ type EPUB struct {
|
|||
Spine []SpineRef
|
||||
Nav *Nav
|
||||
NCX []string // EPUB 2 navPoint hrefs, in order; nesting is not needed to test flattening
|
||||
// NCXBroken writes bytes that are not an NCX, for a book whose spine names one it cannot parse.
|
||||
NCXBroken bool
|
||||
// NCXMissing names an NCX in the spine and omits its bytes.
|
||||
NCXMissing bool
|
||||
Guide []GuideRef
|
||||
}
|
||||
|
||||
|
|
@ -132,13 +141,19 @@ func (e EPUB) BuildAt(t *testing.T, path string) {
|
|||
if href == "" {
|
||||
href = "nav.xhtml"
|
||||
}
|
||||
chapters = append(chapters, Chapter{ID: "navdoc", Href: href, Properties: "nav", Raw: true, Body: navXHTML(*e.Nav)})
|
||||
chapters = append(chapters, Chapter{ID: "navdoc", Href: href, Properties: "nav", Raw: true,
|
||||
Body: navXHTML(*e.Nav), Declared: e.Nav.Missing})
|
||||
if e.Nav.InSpine {
|
||||
refs = append([]SpineRef{{ID: "navdoc"}}, refs...)
|
||||
}
|
||||
}
|
||||
if len(e.NCX) > 0 {
|
||||
chapters = append(chapters, Chapter{ID: "ncx", Href: "toc.ncx", MType: "application/x-dtbncx+xml", Raw: true, Body: ncxXML(e.NCX)})
|
||||
if len(e.NCX) > 0 || e.NCXBroken || e.NCXMissing {
|
||||
body := ncxXML(e.NCX)
|
||||
if e.NCXBroken {
|
||||
body = "<ncx><navMap><navPoint><unclosed></navMap>" // not an NCX: xml.Unmarshal refuses it
|
||||
}
|
||||
chapters = append(chapters, Chapter{ID: "ncx", Href: "toc.ncx", MType: "application/x-dtbncx+xml",
|
||||
Raw: true, Body: body, Declared: e.NCXMissing})
|
||||
}
|
||||
|
||||
var manifest, spine strings.Builder
|
||||
|
|
@ -161,7 +176,7 @@ func (e EPUB) BuildAt(t *testing.T, path string) {
|
|||
spine.WriteString(`<itemref idref="` + r.ID + `"` + lin + `/>` + "\n")
|
||||
}
|
||||
spineAttr := ""
|
||||
if len(e.NCX) > 0 {
|
||||
if len(e.NCX) > 0 || e.NCXBroken || e.NCXMissing {
|
||||
spineAttr = ` toc="ncx"`
|
||||
}
|
||||
var guide strings.Builder
|
||||
|
|
@ -185,6 +200,9 @@ func (e EPUB) BuildAt(t *testing.T, path string) {
|
|||
// media-type labels it — real epubs mislabel xhtml as application/xml, etc. The entry
|
||||
// name (not the href) decides, because an href may be percent-encoded or fragment-bearing.
|
||||
entry := c.entryName()
|
||||
if c.Declared {
|
||||
continue // promised in the manifest, absent from the archive
|
||||
}
|
||||
if c.Raw {
|
||||
add("OEBPS/"+entry, c.Body)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ type epubTOC struct {
|
|||
// ⚠ It deliberately does NOT count a target that resolved fine but landed on a service page or a
|
||||
// non-linear one. Those are ordinary and expected — an EPUB 2 whose toc lists its own cover would report
|
||||
// a "broken" table of contents on every normal book, and an alarm that fires on the normal case is one
|
||||
// nobody reads. Their exclusion is already counted, once, as DocumentsExcluded.
|
||||
// nobody reads. Their exclusion, when they are service pages, is counted once in Document.Excluded.
|
||||
unresolved int
|
||||
// collapsed counts targets that landed in a document another target had already claimed.
|
||||
//
|
||||
|
|
@ -172,6 +172,13 @@ func parseNavDoc(data []byte, baseDir string) (toc []string, labels map[string]s
|
|||
case html.EndTagToken:
|
||||
switch name, _ := z.TagName(); string(name) {
|
||||
case "a":
|
||||
// Trim on CLOSE, where the whole label is in hand. Real books indent their nav, so an
|
||||
// untrimmed label ships "\n Chapter One\n " as the chapter's source title — while
|
||||
// the NCX half (parseNCX) and the txt half (chapterTitleRaw) both trim. One title, two
|
||||
// answers, decided by which table the book happens to carry.
|
||||
if pending != "" {
|
||||
labels[pending] = strings.TrimSpace(labels[pending])
|
||||
}
|
||||
pending = ""
|
||||
case "nav":
|
||||
navType, depth, pending = "", 0, ""
|
||||
|
|
@ -209,16 +216,20 @@ func (p ncxPoint) flatten(baseDir string, out *[]string, labels map[string]strin
|
|||
}
|
||||
|
||||
// parseNCX reads an EPUB 2 NCX, returning its navPoint targets in document order.
|
||||
func parseNCX(data []byte, baseDir string) (targets []string, labels map[string]string) {
|
||||
//
|
||||
// The parse error is RETURNED, not swallowed. A book whose spine names an NCX whose bytes will not parse
|
||||
// has lost its whole statement about its chapters, and the cut then falls back to the spine — which looks
|
||||
// exactly like a book that never had a table of contents at all.
|
||||
func parseNCX(data []byte, baseDir string) (targets []string, labels map[string]string, err error) {
|
||||
labels = map[string]string{}
|
||||
var d ncxDoc
|
||||
if err := xml.Unmarshal(data, &d); err != nil {
|
||||
return nil, labels
|
||||
return nil, labels, err
|
||||
}
|
||||
for _, p := range d.Points {
|
||||
p.flatten(baseDir, &targets, labels)
|
||||
}
|
||||
return targets, labels
|
||||
return targets, labels, nil
|
||||
}
|
||||
|
||||
// resolveTOC maps TOC hrefs onto spine positions.
|
||||
|
|
@ -325,16 +336,20 @@ func isServiceRole(role string) bool {
|
|||
// when that attribute is missing, as it is in books built by tools that half-migrated to EPUB 3 — the first
|
||||
// item carrying the NCX media type.
|
||||
func ncxPath(opf opfPackage, opfDir string) string {
|
||||
// Resolved through the SAME function every other href in this package uses. It was the one resolver
|
||||
// that percent-decoded nothing, which is how two halves of one package came to disagree the first time.
|
||||
if id := strings.TrimSpace(opf.Spine.TOC); id != "" {
|
||||
for _, it := range opf.Manifest {
|
||||
if it.ID == id {
|
||||
return path.Clean(path.Join(opfDir, it.Href))
|
||||
doc, _ := hrefTarget(opfDir, it.Href)
|
||||
return doc
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, it := range opf.Manifest {
|
||||
if strings.Contains(strings.ToLower(it.MediaType), "dtbncx") {
|
||||
return path.Clean(path.Join(opfDir, it.Href))
|
||||
doc, _ := hrefTarget(opfDir, it.Href)
|
||||
return doc
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -672,3 +672,109 @@ func TestEPUB3LandmarkWithAFragmentDoesNotDeleteTheDocument(t *testing.T) {
|
|||
doc.ServiceRolesInsideDocuments)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNavLabelIsTrimmedLikeItsSiblings pins the answer to be the same whichever table the book carries.
|
||||
//
|
||||
// ⛔ Real books indent their nav. Untrimmed, one title arrives as "\n Chapter One\n " through a
|
||||
// nav and as "Chapter One" through an NCX — the same chapter, two answers, decided by which table the
|
||||
// publisher happened to ship. It goes on the wire as title_raw, so a reader renders the whitespace.
|
||||
// The pack's own fixtures could not catch it: they write labels inline, with no indentation to trim.
|
||||
func TestNavLabelIsTrimmedLikeItsSiblings(t *testing.T) {
|
||||
indented := []byte(`<html xmlns:epub="http://www.idpf.org/2007/ops"><body>
|
||||
<nav epub:type="toc">
|
||||
<ol>
|
||||
<li>
|
||||
<a href="c1.xhtml">
|
||||
Chapter One
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</body></html>`)
|
||||
_, navLabels, _, _ := parseNavDoc(indented, "OEBPS")
|
||||
ncx := []byte(`<ncx><navMap><navPoint><navLabel><text>
|
||||
Chapter One
|
||||
</text></navLabel><content src="c1.xhtml"/></navPoint></navMap></ncx>`)
|
||||
_, ncxLabels, _ := parseNCX(ncx, "OEBPS")
|
||||
|
||||
const key, want = "OEBPS/c1.xhtml", "Chapter One"
|
||||
if navLabels[key] != want {
|
||||
t.Fatalf("nav label = %q, want %q", navLabels[key], want)
|
||||
}
|
||||
if navLabels[key] != ncxLabels[key] {
|
||||
t.Fatalf("one title, two answers: nav %q vs ncx %q", navLabels[key], ncxLabels[key])
|
||||
}
|
||||
}
|
||||
|
||||
// TestADeclaredTableOfContentsThatCannotBeReadIsCounted closes the silence this reader had left in its own
|
||||
// alarm: a book that DECLARES a nav or an NCX and whose bytes cannot be read falls back to the spine and is
|
||||
// sold by characters, with nothing saying why.
|
||||
//
|
||||
// ⛔ THREE CASES, AND THE POINT IS THAT THEY STAY APART. "Declared and unreadable" is not "read, and some of
|
||||
// its targets named nothing" — the first lost the book's whole statement about its chapters, the second lost
|
||||
// part of it. And "declared nothing at all" is not a degradation: going to the spine is that book's correct
|
||||
// path. Merging any two of the three moves the defect into new clothes instead of closing it.
|
||||
func TestADeclaredTableOfContentsThatCannotBeReadIsCounted(t *testing.T) {
|
||||
chapters := []chunktest.Chapter{ch("c1", "c1.xhtml", "one"), ch("c2", "c2.xhtml", "two")}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
book chunktest.EPUB
|
||||
wantUnreadable, wantUnresolved int
|
||||
wantStructure string
|
||||
}{
|
||||
{
|
||||
name: "a nav promised by the manifest and absent from the archive",
|
||||
book: chunktest.EPUB{Chapters: chapters, Spine: refs("c1", "c2"), Nav: &chunktest.Nav{TOC: []string{"c1.xhtml", "c2.xhtml"}, Missing: true}},
|
||||
wantUnreadable: 1, wantStructure: StructureDelimited,
|
||||
},
|
||||
{
|
||||
name: "an NCX named by the spine and absent from the archive",
|
||||
book: chunktest.EPUB{Chapters: chapters, Spine: refs("c1", "c2"), NCXMissing: true},
|
||||
wantUnreadable: 1, wantStructure: StructureDelimited,
|
||||
},
|
||||
{
|
||||
name: "an NCX that is present and is not an NCX",
|
||||
book: chunktest.EPUB{Chapters: chapters, Spine: refs("c1", "c2"), NCXBroken: true},
|
||||
wantUnreadable: 1, wantStructure: StructureDelimited,
|
||||
},
|
||||
{
|
||||
// ⚠ NOT a degradation: this book never claimed to have a table of contents.
|
||||
name: "no table of contents declared at all",
|
||||
book: chunktest.EPUB{Chapters: chapters, Spine: refs("c1", "c2")},
|
||||
wantUnreadable: 0, wantStructure: StructureDelimited,
|
||||
},
|
||||
{
|
||||
// The neighbouring failure, kept distinct: the table WAS read, its targets named nothing.
|
||||
name: "a nav that was read and whose targets dangle",
|
||||
book: chunktest.EPUB{Chapters: chapters, Spine: refs("c1", "c2"), Nav: &chunktest.Nav{TOC: []string{"gone-a.xhtml", "gone-b.xhtml"}}},
|
||||
wantUnreadable: 0, wantUnresolved: 2, wantStructure: StructureDelimited,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
doc, err := ingest(tc.book.Build(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if doc.TOCUnreadable != tc.wantUnreadable {
|
||||
t.Fatalf("toc unreadable = %d, want %d", doc.TOCUnreadable, tc.wantUnreadable)
|
||||
}
|
||||
if doc.TOCUnresolved != tc.wantUnresolved {
|
||||
t.Fatalf("toc unresolved = %d, want %d — the two failures must not merge", doc.TOCUnresolved, tc.wantUnresolved)
|
||||
}
|
||||
if doc.Structure != tc.wantStructure {
|
||||
t.Fatalf("structure = %q, want %q", doc.Structure, tc.wantStructure)
|
||||
}
|
||||
// And whatever it counted must reach a human.
|
||||
notes := doc.IngestNotes()
|
||||
named := false
|
||||
for i := 0; i+1 < len(notes); i += 2 {
|
||||
if notes[i] == "toc_unreadable" {
|
||||
named = true
|
||||
}
|
||||
}
|
||||
if named != (tc.wantUnreadable > 0) {
|
||||
t.Fatalf("alarm names toc_unreadable = %v, want %v (notes: %v)", named, tc.wantUnreadable > 0, notes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,18 +30,18 @@ import (
|
|||
// doc.Ruby (seeding.go). It is offline and deterministic — no LLM, no time/rand — so
|
||||
// the whole path is $0 and reproducible.
|
||||
//
|
||||
// epub v1 is a text extraction (02-mvp:25): read the chapters in spine order,
|
||||
// strip tags, drop inline markup/styling. The ONE thing we do NOT drop is ruby:
|
||||
// epub is a text extraction (02-mvp:25): read the chapters the book's own table of contents declares —
|
||||
// nav (EPUB 3) or NCX (EPUB 2), falling back to the spine only when neither resolves — strip tags, drop
|
||||
// inline markup/styling. The ONE thing we do NOT drop is ruby:
|
||||
// <ruby>base<rt>reading</rt></ruby> carries an author reading of a name/term that a
|
||||
// plain text export would silently lose (the documented hole, 04-unhappy §4). We
|
||||
// keep the BASE in the body and hand the READING to the caller for a glossary lock
|
||||
// (memory v2, step 4) — never injecting it into a prompt here (§7d).
|
||||
|
||||
// chapterSep splits a TXT source into chapters. Form feed (U+000C) is the ASCII
|
||||
// "page/section break": semantically a chapter boundary, invisible in prose, and
|
||||
// untouched by text.NormalizeSource. A txt with no \f is a single chapter (Phase-0 backward
|
||||
// compat: the one-file example stays one chapter). epub carries real chapter
|
||||
// structure in its spine, so it does not use this.
|
||||
// chapterSep is the ASCII form feed (U+000C), the "page/section break". It is a SEPARATOR the file
|
||||
// carries, NOT a statement that a chapter begins — see the Structure constants. It is invisible in prose
|
||||
// and untouched by text.NormalizeSource, and it draws the boundaries only when no header line does
|
||||
// (cutTXT). A txt with neither is a single chapter. epub has its own table of contents and never uses this.
|
||||
const chapterSep = "\f"
|
||||
|
||||
// RubyReading is one captured (base, reading) pair and the chapter it appeared in.
|
||||
|
|
@ -83,6 +83,20 @@ type Document struct {
|
|||
// is exactly the defect no gate can catch. What is left is a human reading the warning, and "excluded: 1"
|
||||
// gives them nothing to doubt. «excluded OEBPS/index_split_124.html as `toc`, 2019 runes» does.
|
||||
Excluded []ExcludedDocument
|
||||
// TOCUnreadable counts tables of contents the book DECLARED and this reader could not read at all —
|
||||
// the manifest names a nav document, or the spine names an NCX, and the bytes are missing or will not
|
||||
// parse.
|
||||
//
|
||||
// ⛔ IT IS NOT TOCUnresolved, and merging the two would hide the worse case inside the milder one.
|
||||
// "Read it, and some of its targets named nothing" is a book with a partly broken table; "declared one
|
||||
// and could not read it at all" is a book whose whole statement about its chapters was lost, after
|
||||
// which the cut silently falls back to the spine and the book is sold by characters. A reader must be
|
||||
// able to tell those apart, because only the second explains why an EPUB with a real nav came back
|
||||
// `delimited`.
|
||||
//
|
||||
// ⚠ A book that declares NO table of contents is not counted here: going to the spine is its normal,
|
||||
// correct path, not a degradation.
|
||||
TOCUnreadable int
|
||||
TOCUnresolved int // table-of-contents targets that named nothing this book reads
|
||||
TargetsCollapsed int // targets that shared a document with an earlier one, so the cut is coarser
|
||||
ServiceRolesInsideDocuments int // toc/cover roles declared at a POINT inside a document, so NOT excluded
|
||||
|
|
@ -113,6 +127,7 @@ func (d *Document) IngestNotes() []any {
|
|||
if n := len(d.Excluded); n > 0 {
|
||||
kv = append(kv, "documents_excluded", n, "excluded", d.Excluded)
|
||||
}
|
||||
add("toc_unreadable", d.TOCUnreadable)
|
||||
add("toc_unresolved", d.TOCUnresolved)
|
||||
add("targets_collapsed", d.TargetsCollapsed)
|
||||
add("service_roles_inside_documents", d.ServiceRolesInsideDocuments)
|
||||
|
|
@ -291,7 +306,8 @@ func cutTXT(decoded string, st *lang.SourceStructure) (chapters []string, witnes
|
|||
// legitimate subtitle → merged chapters).
|
||||
const chapterHeaderMaxRunes = 60
|
||||
|
||||
// isCJKChapterHeader reports whether a single line is a chapter header for the given unit rune. It
|
||||
// isChapterHeader reports whether a single line is a chapter header for the given unit rune, under the
|
||||
// book's resolved source structure. It
|
||||
// requires: a short trimmed line (≤60 runes); a leading 第<numerals>; the unit rune immediately
|
||||
// after the numerals; and — critically — the char AFTER the unit is a SEPARATOR (whitespace / ::、,.
|
||||
// / dash) or end-of-line, NOT a content glyph. The separator guard is the precision fix (self-
|
||||
|
|
@ -341,7 +357,8 @@ func detectChapterUnit(lines []string, st *lang.SourceStructure) rune {
|
|||
return 0
|
||||
}
|
||||
best, bestN := rune(0), 0
|
||||
// The section markers (章节節回) are shared lang.CJKSection data (pair-14 addendum-A); iterated in AUTHORED
|
||||
// The section units come from the book's RESOLVED source structure (lang.SourceStructure — its own
|
||||
// <lang>/structure.txt, else the embedded CJK default); iterated in AUTHORED
|
||||
// order so a tie between two units breaks deterministically (first-seen), as the fixed slice once did.
|
||||
for _, unit := range st.UnitOrdered {
|
||||
n := 0
|
||||
|
|
@ -728,8 +745,14 @@ func ingestEPUB(p string) (*Document, error) {
|
|||
}
|
||||
var navTargets []string
|
||||
labels := map[string]string{}
|
||||
tocUnreadable := 0
|
||||
if navEntry != "" {
|
||||
if data, rerr := readZipEntry(files, navEntry); rerr == nil {
|
||||
data, rerr := readZipEntry(files, navEntry)
|
||||
if rerr != nil {
|
||||
// The manifest SAYS this book has a navigation document. Falling through to the spine without
|
||||
// saying so is the silence this counter exists to end.
|
||||
tocUnreadable++
|
||||
} else {
|
||||
targets, navLabels, landmarks, insideDoc := parseNavDoc(data, path.Dir(navEntry))
|
||||
navTargets, labels = targets, navLabels
|
||||
serviceInsideDoc += insideDoc
|
||||
|
|
@ -770,8 +793,15 @@ func ingestEPUB(p string) (*Document, error) {
|
|||
// book. Falling through to the NCX must not erase that count along with the failed table.
|
||||
navUnresolved := toc.unresolved
|
||||
if ncxEntry := ncxPath(opf, opfDir); ncxEntry != "" {
|
||||
if data, rerr := readZipEntry(files, ncxEntry); rerr == nil {
|
||||
targets, ncxLabels := parseNCX(data, path.Dir(ncxEntry))
|
||||
data, rerr := readZipEntry(files, ncxEntry)
|
||||
switch {
|
||||
case rerr != nil:
|
||||
tocUnreadable++ // the spine names an NCX and its bytes are not there
|
||||
default:
|
||||
targets, ncxLabels, perr := parseNCX(data, path.Dir(ncxEntry))
|
||||
if perr != nil {
|
||||
tocUnreadable++ // the bytes are there and are not an NCX
|
||||
}
|
||||
if ncx := resolveTOC(tocNCX, targets, docs); ncx.kind == tocNCX {
|
||||
// Switching tables: carry the nav's dangling count across, or it vanishes with the table.
|
||||
toc, labels = ncx, ncxLabels
|
||||
|
|
@ -833,7 +863,7 @@ func ingestEPUB(p string) (*Document, error) {
|
|||
}
|
||||
doc := &Document{
|
||||
DocumentsAttached: attached, Excluded: excluded,
|
||||
TOCUnresolved: toc.unresolved, TargetsCollapsed: toc.collapsed,
|
||||
TOCUnreadable: tocUnreadable, TOCUnresolved: toc.unresolved, TargetsCollapsed: toc.collapsed,
|
||||
ServiceRolesInsideDocuments: serviceInsideDoc,
|
||||
}
|
||||
denseNo := 0 // matches SplitChunks: only a NON-empty chapter takes a number
|
||||
|
|
|
|||
|
|
@ -574,7 +574,8 @@ func TestExtractXHTMLRejectsNonUTF8Charset(t *testing.T) {
|
|||
|
||||
func TestIngestEPUBPercentEncodedHref(t *testing.T) {
|
||||
// Real epubs percent-encode non-ASCII (and spaced) filenames in the manifest while the zip
|
||||
// entry carries the decoded name. Without resolveHref's url.PathUnescape the entry is missed
|
||||
// entry carries the decoded name. Without the percent-decode in hrefTarget (which resolveHref wraps)
|
||||
// the entry is missed
|
||||
// and ingest fails loud on a chapter that is actually present.
|
||||
chapters := []chunktest.Chapter{
|
||||
{ID: "c1", Href: "%E7%AC%AC%E4%B8%80%E7%AB%A0.xhtml", EntryName: "第一章.xhtml", Body: `<p>第一章。</p>`},
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ func TestEveryIngestCountReachesTheAlarm(t *testing.T) {
|
|||
f := rt.Field(i)
|
||||
d := &Document{Chapters: []string{"x"}}
|
||||
fv := reflect.ValueOf(d).Elem().Field(i)
|
||||
// Every field that carries an alarm, whatever its type. Restricting this to ints is how the guard
|
||||
// would quietly stop covering a count the day one grows into a list.
|
||||
// The field types that carry an alarm TODAY: ints, and the one slice a count grew into. ⚠ This switch
|
||||
// is the guard's OWN limit — a counter of any other type falls into `default` and is skipped silently,
|
||||
// so a new alarm field must be added here as well as to IngestNotes.
|
||||
switch {
|
||||
case f.Type.Kind() == reflect.Int:
|
||||
fv.SetInt(1)
|
||||
|
|
|
|||
|
|
@ -400,7 +400,8 @@ func denseFrom(doc *chunk.Document, keptIdx []int) denseChapters {
|
|||
return out
|
||||
}
|
||||
|
||||
// buildManifest projects a fresh split into the document. chapterTexts is index-aligned to chapter
|
||||
// buildManifest projects a fresh split into the document. ch holds the chapters that took a number and
|
||||
// their source titles, index-aligned to chapter
|
||||
// numbers (chunk.SplitChunksWithChapters), chunks is the split itself. Pure and deterministic.
|
||||
func (r *Runner) buildManifest(ch denseChapters, chunks []chunk.Chunk, structure string, src sourceFingerprint) *BookManifest {
|
||||
m := &BookManifest{
|
||||
|
|
@ -452,8 +453,12 @@ func (r *Runner) buildManifest(ch denseChapters, chunks []chunk.Chunk, structure
|
|||
"book", r.Book.BookID, "chapter", u.Chapter, "chapter_texts", len(ids))
|
||||
}
|
||||
title := ""
|
||||
if u.Chapter-1 < len(ch.titles) {
|
||||
title = ch.titles[u.Chapter-1] // Number is dense and 1-based, so it indexes the dense slice
|
||||
// The SAME guard the chapter-id lookup six lines above carries, and for the same reason: that
|
||||
// branch exists because dense 1-based numbering might one day stop holding, and it degrades with
|
||||
// a WARN. Without the lower bound this line turns that warned degradation into an index-out-of-
|
||||
// range panic on a paid run.
|
||||
if k := u.Chapter - 1; k >= 0 && k < len(ch.titles) {
|
||||
title = ch.titles[k]
|
||||
}
|
||||
m.Chapters = append(m.Chapters, ManifestChapter{ID: id, Number: u.Chapter, Heading: headings[u.Chapter], TitleRaw: title})
|
||||
i = len(m.Chapters) - 1
|
||||
|
|
|
|||
150
docs/BACKEND_CONSISTENCY_SESSION_PROMPT.md
Normal file
150
docs/BACKEND_CONSISTENCY_SESSION_PROMPT.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Промт: бэкенд-сессия «ЗАКОН БАНКА — ЧЕСТНО» (пак одного предмета)
|
||||
|
||||
## 1. Какая проблема и что решит твой результат
|
||||
|
||||
Приоритет владельца №1 — **консистентность терминов и голосов на всю книгу**. Она держится на банке
|
||||
памяти: строка «термин → перевод» едет в промпт как ЗАКОН для каждой роли. Сегодня закон исполняется,
|
||||
но **три места описывают его неверно, а одно не исполняет собственный совет**. Цена — не поломка, а
|
||||
хуже: мы не можем узнать правду о собственной работе. Метрика «промахов нет» означает «банк исполнен»,
|
||||
а читается как «исполнен ВЕРНО» — и ровно в случае, которого владелец боится (вложили неверный перевод,
|
||||
модель послушалась), прибор молчит надёжнее всего.
|
||||
|
||||
Твой результат: закон банка описан тем, что код делает, и один настоящий пробел в нём закрыт.
|
||||
|
||||
## 2. Зона и git
|
||||
|
||||
**Зона записи: `backend/`.** Чужие зоны (`platform/`, `frontend/`, `eval/`, `docs/`) — только чтение;
|
||||
расхождения — пингом в секцию «Бэкенд» файла `docs/PROGRESS.md`.
|
||||
**Сессия НЕ коммитит** — готовое дерево передаёшь оркестратору. Канон git-дисциплины — `CLAUDE.md`.
|
||||
`.env` не читать. Не работать под `/tmp`.
|
||||
|
||||
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
|
||||
|
||||
1. `CLAUDE.md` — цели владельца и гардрейлы.
|
||||
2. Строки бэклога **267**, **318**, **320** (`docs/PROGRESS.md`, греп `^| N |`).
|
||||
3. `backend/README.md` — механика движка.
|
||||
4. `docs/architecture/03-implementation-notes.md` — через баннер.
|
||||
5. `docs/architecture/12-go-style-notes.md` — только больные места.
|
||||
|
||||
**Ратифицированное, что нужно для работы, — в теле промта, чтобы не искать (`D39.216` требует так):**
|
||||
|
||||
- **Закон банка (`D39.104`)**: строка на проводе — закон для КАЖДОЙ роли и КАЖДОЙ строки, независимо от
|
||||
статуса. ⛔ Ослаблять его этот пак НЕ предлагает: он ратифицирован, доказательств против нет ни у
|
||||
одного голоса. Предмет — исполнение и честность описания.
|
||||
- **`D39.216` (мандат владельца, 06.09), делай РОВНО так**: проектировать и чинить **без хаков и
|
||||
воркэраундов**; где нынешняя форма не тянет — **развязать или переписать**, а не обложить заплатой.
|
||||
«Дешевле подпереть» доводом НЕ является. Отступление законно, но **объявляется с ценой**.
|
||||
- **Комментарий не доказывает поведение** (`D39.214` п.3): источник утверждения о СМЫСЛЕ — тест или
|
||||
прогон. ⚠ Эту норму НЕ ищи грепом в `CLAUDE.md` — её там нет, она живёт нотой.
|
||||
- **Число называет ДЕРЕВО и ПОПУЛЯЦИЮ**: приводишь счёт — назови коммит и что именно посчитано.
|
||||
- **Рядом с нулём — контрольная величина**: «0 совпадений» неотличимо от «искал не там» (`CLAUDE.md`).
|
||||
- **Мутация засчитывается по ТЕКСТУ сообщения, а не по факту красноты** (`CLAUDE.md`): правый вердикт
|
||||
по неправой причине — дыра, а не поимка.
|
||||
- **Тесты и гейты не подгонять под зелень** (`D39.121`, мотив решает).
|
||||
|
||||
## 4. Состав и разметка свободы
|
||||
|
||||
### 4.1 Гард перекрытия окон при слиянии (строка 267) — **делай РОВНО так**
|
||||
|
||||
Один термин в одном смысле может нести две конкурирующие формы, и обе доезжают на оба провода — дыра в
|
||||
проверке схлопывания. Решение владельца 05.09 состоялось, строка несёт разбор. Закрыть.
|
||||
|
||||
### 4.2 Совет, который движок не исполняет (строка 320а) — **реши сам, аргументируй**
|
||||
|
||||
Диагностика советует оператору пометить одну строку статусом, а гвард полисемии **статус не смотрит
|
||||
вовсе**. Два честных выхода: привести совет к тому, что код делает, **или** научить гвард смотреть
|
||||
статус. **Выбор объяви и обоснуй; молча не решай.** ⚠ Второй вариант меняет поведение — тогда это смена
|
||||
заказа, и нужен пинг, а не правка.
|
||||
|
||||
### 4.3 Комментарий постчека — привести к поведению (строка 318) — **делай РОВНО так**
|
||||
|
||||
Комментарий обещает ловить и случай «вложили неверный перевод, модель исполнила»; код при найденной
|
||||
форме делает `continue` ⇒ промаха нет. Приведи текст к поведению **и поставь пин**, чтобы описание
|
||||
больше не могло разойтись молча.
|
||||
|
||||
⛔ **Детектор правильности ЗНАЧЕНИЯ здесь НЕ строить.** Он живёт строкой **286**, упирается в замер
|
||||
точности, а не в код, и его постройка тут будет расширением заказа.
|
||||
|
||||
### 4.4 Два ложных комментария — снять (строки 321 и 322) — **делай РОВНО так**
|
||||
|
||||
- Оправдание `nil`-переноса в редакторской волне («единица есть глава или её часть») — **ложно**: глава
|
||||
регулярно делится на несколько правочных единиц. ⛔ **Сам перенос НЕ строить** — по строке 321 сначала
|
||||
замер дрейфа, инцидентов пока ноль. Здесь только честный комментарий.
|
||||
- Утверждение, что обычный резюм перезаписывает строку статуса, — **ложно**: он выходит раньше, пишет
|
||||
только ре-пин.
|
||||
|
||||
### 4.5 Чего в паке НЕТ и почему — **не делай**
|
||||
|
||||
- **Строка 322 (история принятой редактуры)** — другой предмет: жизненный цикл чекпойнтов и схема, а не
|
||||
закон банка. Отдельным паком.
|
||||
- **Строка 295 (подать подписанный канон в прогон)** — снята из этого пака: у бэкенд-сессии нет ни
|
||||
книги, ни стенда, ни санкции на деньги. Если найдёшь чисто бэкендовую половину — **назови её пингом**,
|
||||
но не бери.
|
||||
- **Перекрой структуры глав (161/283)** — отдельный дизайн-пак.
|
||||
|
||||
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — механизм назван, и пустое исполнение его не удовлетворяет
|
||||
|
||||
⛔ «Перечитал сам» **не считается**. Обязательны все три:
|
||||
|
||||
1. **Мутационная проверка каждого пина, который ты заводишь или трогаешь.** Механизм: сломай проверяемое
|
||||
свойство, прогони пин, **прочитай ТЕКСТ падения** и убедись, что он говорит про сломанное тобой, а не
|
||||
про что-то соседнее; восстанови файл и **сверь sha256** с исходным. Артефакт: таблица «мутация →
|
||||
текст падения → хеш восстановлен».
|
||||
2. **Опровергатель твоей готовой работы отдельным субагентом.** ⚠ Харнесс по умолчанию субагентов не
|
||||
спавнит — **тебе это РАЗРЕШЕНО и требуется**. Модель задай ЯВНО (`fable` — контролёр проекта), знай,
|
||||
сколько их у тебя работает; одного-двух достаточно. Мандат ему: «найди в этой работе отсутствующее и
|
||||
противоречащее», а не «проверь, всё ли хорошо».
|
||||
3. **Греп по отозванной формулировке перед словом «исправлено»**: `grep -c "<снятая фраза>" <файл>` —
|
||||
ноль либо только внутри пометки. Смена 06.09 попалась на этом трижды.
|
||||
|
||||
## 6. Оси ревью — характер работы «Код» и «Доки»
|
||||
|
||||
Из таблицы приёмки, две оси, обе твои:
|
||||
- **Код:** ⚠ «тест утверждает не то, что думает» — проверь, что каждый твой пин падает ИМЕННО от того,
|
||||
что он охраняет (см. §5.1).
|
||||
- **Доки:** сверка утверждений против кода и живых носителей — весь этот пак ровно об этом.
|
||||
|
||||
## 7. Записка-план ДО работы
|
||||
|
||||
До первой правки пришли оркестратору ≤15 строк: что берёшь по каждому из четырёх пунктов · где ждёшь
|
||||
сопротивления · что считаешь спорным в самом заказе. Это дешевле, чем разойтись на готовом.
|
||||
|
||||
## 8. Заявление = команда
|
||||
|
||||
Любое число и любая категорика в отчёте — **с командой, которой получены**, и её выводом, а не
|
||||
пересказом. «Батарея зелёная» без команды и её кода возврата — не факт.
|
||||
|
||||
## 9. Эхо-протокол старта
|
||||
|
||||
ДО работы пришли ≤10 строк: **скоуп** (что берёшь) · **инварианты** (что обязано уцелеть) · **не-делать**
|
||||
(что в паке запрещено). Если эхо разойдётся с моим заказом — лучше здесь, чем в приёмке.
|
||||
|
||||
## 10. Что НЕ удалось — обязательная секция отчёта
|
||||
|
||||
Что не проверено · что не воспроизвелось · где данных не хватило. **«Не измерено» вместо догадки.**
|
||||
|
||||
## 11. Канал вопросов и право отказаться
|
||||
|
||||
Конфликт промта с кодом или доками — **пинг, не правка**. И у тебя есть **право сказать «этого делать не
|
||||
надо»** с аргументом: если по коду окажется, что предмет описан мимо, это самый ценный ответ.
|
||||
|
||||
## 12. Прямой канал
|
||||
|
||||
Механизм — `CLAUDE.md` §«Связь между сессиями»: файл `/tmp/textmachine-channel`, свой блок впиши первым
|
||||
делом, адреса других ролей — оттуда, живость сверяй `ListAgents`. ⚠ **Нужной роли нет или файла нет —
|
||||
это НОРМАЛЬНЫЙ случай**: вопрос секцией в отчёт, сессии подряд не опрашивать.
|
||||
|
||||
## Деньги
|
||||
|
||||
**Этот пак — $0.** Платных вызовов к моделям в нём нет ни одного: все четыре предмета — код,
|
||||
комментарии и пины на фикстурах. ⛔ Если по ходу окажется, что предъявить что-то можно только платным
|
||||
прогоном — **это пинг оркестратору за санкцией, а не твоё решение**. Санкции на деньги промт не несёт.
|
||||
|
||||
## Отчёт
|
||||
|
||||
Секция «Бэкенд» в `docs/PROGRESS.md` + сообщение оркестратору: что построено · чем предъявлено (команда
|
||||
и вывод) · таблица мутаций из §5.1 · что сказал опровергатель и что ты с этим сделал · двигает ли
|
||||
что-нибудь из сделанного снапшот, хеш запроса, ключ манифеста или версию встроенных данных · секция §10 ·
|
||||
**что ты сам считаешь слабым местом своей работы**.
|
||||
|
||||
⚠ Числа батареи снимай **после последней правки**, не до.
|
||||
|
|
@ -26,8 +26,29 @@ Pathspec-форма `git commit -- <пути>` защищает от чужог
|
|||
зоны `docs/`, в который ПИШУТ чужие зоны (секции «Бэкенд»/«Полигон», решение владельца). Итог: твой
|
||||
коммит про свою строку бэклога уносит чужой отчёт под твоим сообщением. Содержимое цело, теряется
|
||||
атрибуция; **историю НЕ переписывать** — сообщить владельцу и зоне.
|
||||
⇒ **Исполнимый шаг, а не «быть внимательным»: ПЕРЕД каждым коммитом `docs/PROGRESS.md` смотри
|
||||
`git diff -- docs/PROGRESS.md` и опознай, есть ли там ЧУЖИЕ секции.** Есть — либо коммить их отдельным
|
||||
⇒ **Исполнимый шаг, а не «быть внимательным»: ПЕРЕД каждым коммитом ЛЮБОГО ЗОННОГО ЖУРНАЛА смотри
|
||||
`git diff -- <файл>` и опознай, есть ли там ЧУЖИЕ секции.** ⛔ **Список полный, и он НЕ сводится к
|
||||
`docs/PROGRESS.md`:** `docs/PROGRESS.md` (секции «Бэкенд»/«Полигон») · `platform/docs/platform-PROGRESS.md` ·
|
||||
`frontend/docs/frontend-PROGRESS.md` · `platform/docs/DEFECT_REGISTER.md`. ⚠ **Первая редакция этого шага
|
||||
называла ОДИН файл, и ловушка сработала третий раз — на `platform-PROGRESS.md`**: правило было писано
|
||||
про ЭКЗЕМПЛЯР, а не про КЛАСС, и потому не сработало там, где класс тот же. Класс: любой файл, куда
|
||||
пишет зона, а коммитишь ты.
|
||||
⛔ **СТАТУС-БОРД УКАЗЫВАЕТ, А НЕ КОПИРУЕТ — иначе одна правка становится четырьмя.** Замерено 06.09:
|
||||
один денежный факт («платформенная половина построена, ставки за главу нет») жил В ЧЕТЫРЁХ файлах —
|
||||
`15-money-path.md`, шапка `09-target-architecture.md`, `17-seam-inbound-law.md`, ряд бэклога. Сплошной
|
||||
аудит увидел его как ОДИННАДЦАТЬ находок, и на каждую я писал правку заново; когда факт сменился,
|
||||
менять пришлось все четыре, и три из них — пересказ. ⇒ **Механика живёт у своего ПРЕДМЕТА (денежный
|
||||
путь — в `15-money-path.md`); борды и реестры несут СТАТУС плюс указатель на носитель.** Правило
|
||||
проверяется вопросом «если этот факт завтра изменится, сколько файлов придётся тронуть?» — больше
|
||||
одного значит копия. ⚠ Не относится к ЖУРНАЛУ решений: тела нот — улика по `D23.3`, они описывают
|
||||
дерево на ДАТУ и не пере-писываются даже когда протухли; им нужна эррата, а не указатель.
|
||||
|
||||
⛔ **ВТОРАЯ ЛОВУШКА ТОГО ЖЕ РОДА — ПЕРЕ-НАВЕДЕНИЕ ЯКОРЕЙ СПЛОШНОЙ ЗАМЕНОЙ.** Правя протухшие `file:line`,
|
||||
легко заменить строку пути ВЕЗДЕ. Но тот же путь стоит в доках дважды в разных ролях: как ЖИВАЯ ССЫЛКА
|
||||
(её чинить надо) и как УЛИКА в описании дефекта — «код цитирует вот этот путь, а его удалили» (её
|
||||
чинить НЕЛЬЗЯ: замена превращает описание поломки в описание её отсутствия). Замерено 06.09: так была
|
||||
заландена ПЕРЕВЁРНУТОЙ клауза строки бэклога 299. ⇒ **перед сплошной заменой пути грепни его и раздели
|
||||
хиты на ссылки и улики; в описаниях дефектов путь не трогай.** Есть — либо коммить их отдельным
|
||||
коммитом с сообщением о ЧУЖОЙ работе (тогда атрибуция в сообщении, а не в авторстве), либо дождись,
|
||||
пока зона допишет, и назови её в сообщении. Знание «этот файл общий» было записано ещё утром и не
|
||||
помогло — потому что не называло ДЕЙСТВИЯ. **Функции:** промты сессий → приёмки → ратификации
|
||||
|
|
@ -236,7 +257,38 @@ Y»), а не «посмотри». По умолчанию **Fable 5**, при
|
|||
дифф, если менялись только версии/хеши.
|
||||
- Прод-инъекцию воспроизводить `tmctl translate` + `LOG_LLM_BODIES` байт-диффом (не по
|
||||
`injected_ids`); порт-зеркала — «при расхождении верить Go».
|
||||
- ⛔ **«ИСПРАВЛЕНО» ГОВОРИТСЯ ТОЛЬКО ПОСЛЕ ГРЕПА ПО ОТОЗВАННОЙ ФОРМУЛИРОВКЕ, а не после того, как ты её
|
||||
исправил в голове.** Шаг: `grep -c "<снятая фраза>" <файл>` — ноль либо только внутри пометки-эрраты,
|
||||
цитирующей отозванное. Замер 06.09: класс сработал ТРИЖДЫ за смену — «внесён в состав минора» о правке,
|
||||
которой в каноне не было (эррата 06.09-а) · «исправлено вместе с остальными» напарнику о носителе, до
|
||||
которого правка не дошла · и он же остался немаркированным. **Пишущий отчёт описывает своё НАМЕРЕНИЕ и
|
||||
читает его как состояние файла** — греп стоит секунду и различает их.
|
||||
- ⛔ **ТО ЖЕ ПРО «ЗАПИСАНО»: ПИСЬМО, ОБЪЯВЛЯЮЩЕЕ НАХОДКУ ЗАВЕДЁННОЙ, САМО НОСИТЕЛЕМ НЕ ЯВЛЯЕТСЯ.**
|
||||
Формулировка строителя; замер 06.09: за смену **ПЯТЬ** находок жили только в переписке — две находки
|
||||
платформенной зоны без рядов · нечитаемое оглавление · непрогоняемый каталог мутаций · корректность
|
||||
разреза · манифестная половина EPUB на живых книгах. **Две из них я лично объявил учтёнными** («твой
|
||||
инвариант вошёл в тот же ряд»), не заведя строки, — и обе нашёл не я, а автор находки, пойдя проверять
|
||||
не чужие правки, а СВОИ УТВЕРЖДЕНИЯ о том, что всё учтено. ⇒ перед словом «завёл» — `grep` по ЖИВОМУ
|
||||
бэклогу НА ПРЕДМЕТ находки, а не на её название: название совпадёт с соседней строкой, предмет — нет.
|
||||
- ⛔ **ЧИСЛО НАЗЫВАЕТ ДВЕ ВЕЩИ: ДЕРЕВО И ПОПУЛЯЦИЮ.** Дерево — sha коммита либо «рабочее дерево + что в нём
|
||||
было незакоммичено». Популяция — ЧТО именно посчитано, потому что два честных прибора считают разное и
|
||||
расходятся молча. Замер 06.09, стоивший верной записи: я написал «851 верхнеуровневых теста», верификатор
|
||||
предъявил «на всех коммитах 850 или меньше» и потребовал снять, я снял — а число было ВЕРНО. Он считал
|
||||
`func Test`, мой прибор считает верхнеуровневые ПРОГОНЫ вывода `go test -v`, то есть `Test` **плюс `Fuzz`**;
|
||||
у платформы два `Fuzz`-таргета, и вся разница была в них (`93f098c`: 849+2=**851**).
|
||||
- ⛔⛔ **ПРОВЕРКА ЧУЖОЙ НАХОДКИ ЕГО ЖЕ МЕТОДОМ — НЕ ПРОВЕРКА.** Тем же замером 06.09 я «пере-мерил сам, а не
|
||||
поверил» — и воспроизвёл его команду, а значит и его ошибку; совпадение доказало лишь, что мы оба считаем
|
||||
одно и то же не то, и на этом «подтверждении» я заландил снятие ВЕРНОГО числа. **Бери ДРУГОЙ прибор:**
|
||||
другую популяцию, другую команду, другую сторону улики. Совпавший результат стоит ровно столько, сколько
|
||||
стоит независимость инструмента.
|
||||
- ⚠ **Правя носитель, ставь на исправленные места пометку тем же движением** (норма `535f60a`): эррата без
|
||||
маркера на хвосте оставляет прежнюю формулировку стоять немаркированной, и следующий читатель возьмёт её.
|
||||
- Промт-архивация: только отработанные, с баннером-исходом; residual-трекеры держать активными.
|
||||
⚠ **ПРИЁМКА ПАКА И РЕТИРОВАНИЕ ЕГО ПРОМТА — ОДИН АКТ, не два дела подряд.** Пока промт стоит в
|
||||
таблице активных, отработанный пак остаётся ЖИВЫМ ЗАДАНИЕМ: холодная сессия по `CLAUDE.md` идёт в эту
|
||||
таблицу ПЕРВЫМ делом и берёт оттуда свою работу. Замер 06.09: форма заказа принята в 00:47, промт снят
|
||||
в 01:43 — 56 минут окна; движковый пак принят в 01:43, промт снят в 01:51, то есть ПОВТОР за один час
|
||||
(эррата 06.09-в). Снимай промт тем же заходом, что пишешь акт приёмки.
|
||||
|
||||
## Чего НЕ делать
|
||||
|
||||
|
|
|
|||
169
docs/PLATFORM_MONEY_TRUTH_SESSION_PROMPT.md
Normal file
169
docs/PLATFORM_MONEY_TRUTH_SESSION_PROMPT.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# Промт: платформенная сессия «ДЕНЬГИ И ПРАВДА НА ЭКРАНЕ» — первая половина критерия впуска
|
||||
|
||||
## 1. Какая проблема и что решит твой результат
|
||||
|
||||
Проект готовится впустить **первого платящего чужого человека**. Критерия впуска в репозитории нет
|
||||
вовсе (строка бэклога **326**) — и этот пак пишет его первую половину **делом**, а не формулировкой.
|
||||
|
||||
Сегодня платформа врёт человеку тремя способами и один раз молчит:
|
||||
- **деньги считаются по нижней границе**: отменённые вызовы, УЖЕ ушедшие к провайдеру, не записываются
|
||||
никуда (`PD-441`, замер: три отменённых вызова прожили по две минуты с ценой `0`, соседние
|
||||
завершённые стоили $0.017 и $0.063);
|
||||
- **заклинивший прогон невидим**: живой прогон с намертво заблокированной расплатой не показывается ни
|
||||
на одной поверхности и не лечится штатной ручкой; вывести его может только пользователь, нажав Stop,
|
||||
и **никто ему об этом не говорит** (`PD-424`);
|
||||
- **чужие события ложились на оплаченную попытку** — чужой потолок ставил `paused` живому оплаченному
|
||||
прогону, чужой `unit_done` писал главы, которых никто не покупал (`PD-438`);
|
||||
- **экран врёт о единице**: число под подписью «Знаков» считает руны потока записи файла, а для EPUB —
|
||||
руны ZIP-архива (строка **282**).
|
||||
|
||||
И отдельно: **вход шире выдачи** — интейк принимает форматы, для которых выдача отдаёт книгу одним
|
||||
полотном (строка **325**). Компенсация «на бете просто не впустим» сегодня — намерение, а не факт: она
|
||||
требует кода.
|
||||
|
||||
Твой результат: человек, заплативший деньги, видит правду о них — и не может застрять молча.
|
||||
|
||||
## 2. Зона и git
|
||||
|
||||
**Зона записи: `platform/`.** Чужие зоны (`backend/`, `frontend/`, `eval/`, `docs/`) — только чтение;
|
||||
пинги и итоги — **зонный журнал** `platform/docs/platform-PROGRESS.md` (решение владельца), в
|
||||
`docs/PROGRESS.md` платформа не пишет.
|
||||
**Сессия НЕ коммитит** — дерево передаёшь оркестратору. Канон git-дисциплины — `CLAUDE.md`.
|
||||
`.env` не читать.
|
||||
|
||||
## 3. Карта чтения — ЗАКОН
|
||||
|
||||
1. `CLAUDE.md` — цели владельца и гардрейлы.
|
||||
2. `platform/docs/ENGINEERING_STANDARDS.md` — **нормы зоны и критерии приёмки (Definition of Done)**.
|
||||
⚠ Ссылка на этот файл обязана быть в каждом платформенном промте; из семи прежних сослались пять,
|
||||
и норма дважды проскользнула. Здесь она стоит.
|
||||
3. `platform/docs/DEFECT_REGISTER.md` — ряды `PD-441`, `PD-424`, `PD-438`.
|
||||
4. Строки **282**, **325**, **326** бэклога (`docs/PROGRESS.md`, греп `^| N |`).
|
||||
5. `platform/README.md` + зонный журнал (шапка, не целиком — он 5029 строк, см. §4.0).
|
||||
|
||||
**Ратифицированное — в теле промта:**
|
||||
|
||||
- **`D39.216` (мандат владельца, 06.09), делай РОВНО так**: чинить **без хаков и воркэраундов**; где
|
||||
форма не тянет — развязать или переписать. «Дешевле подпереть» доводом НЕ является; отступление
|
||||
законно, но **объявляется с ценой**.
|
||||
- **Продуктовых КВОТ и фри-тир-лимитов нет и не проектируется**; оплаты в бете нет, пополнение — из
|
||||
админки. Первую внешнюю книгу впускает владелец.
|
||||
- **Число называет ДЕРЕВО и ПОПУЛЯЦИЮ** · **рядом с нулём — контрольная величина** · **мутация
|
||||
засчитывается по ТЕКСТУ сообщения, а не по факту красноты** · **комментарий не доказывает поведение**
|
||||
(`D39.214`; грепом в `CLAUDE.md` не ищи, там её нет).
|
||||
- **Тесты и гейты не подгонять под зелень** (`D39.121`, решает мотив).
|
||||
|
||||
## 4. Состав и разметка свободы
|
||||
|
||||
### 4.0 Триаж зонного журнала — **делай РОВНО так**, первым, до кода
|
||||
|
||||
Журнал вырос до **5029 строк / 42 секций**, и две новейшие занимают 40 %. ⭐ Конвенция слайсов у зоны **своя и уже
|
||||
работает**: в `platform/docs/archive/` лежат `platform-PROGRESS-P0-P3`, `-P4-P6`, `-P7`, `-P8`. Она остановилась.
|
||||
|
||||
⛔ **НО РЕЗАТЬ СРАЗУ НЕЛЬЗЯ, и вот замер, купленный 06.09.** Оркестратор проделал ту же операцию над
|
||||
`STACK_DECISIONS.md` и едва не уничтожил свод: секции «Что решено сессией P…» выглядят хроникой по заголовку, а
|
||||
внутри — **реестр из 36 сплошно нумерованных живых решений**. Ошибка была в том, что судили по ЗАГОЛОВКУ, не
|
||||
прочитав содержимое. ⇒ **в журнале сделай тот же тест ПРЕЖДЕ выноса.**
|
||||
|
||||
Машинный проход по выносимому (он уже написан контролёром, воспроизводится за секунду): какие `PD-*` и «строки N»
|
||||
упомянуты и все ли разрешаются; и главное — **какие ⛔/⚠⚠-утверждения не ссылаются ни на ряд регистра, ни на строку
|
||||
бэклога, ни на D-ноту**. Замер 06.09 по двум новейшим секциям: **69 таких**, в основном «найдено и починено до
|
||||
сдачи». По норме регистра починенное тоже получает ряд ⇒ **это список на чтение, а не на вынос**.
|
||||
|
||||
**Приёмка §4.0:** проход печатает **ноль** утверждений без носителя · вынесено только то, что при чтении оказалось
|
||||
настоящей хроникой · всё, что оказалось правилом или незакрытой находкой, получило ряд регистра или строку бэклога
|
||||
ПЕРЕД выносом.
|
||||
|
||||
### 4.1 Три мажора — **делай РОВНО так** в предмете, **реши сам** в форме
|
||||
|
||||
`PD-441` · `PD-424` · `PD-438`. Все три про одно: **деньги и видимость чужого прогона**, и человек
|
||||
жалуется одинаково — «списали не то / висит / молчит». Форму лечения выбираешь ты; предмет не двигаешь.
|
||||
|
||||
⚠ По `PD-441` отдельно: если полная запись цены отменённого вызова недостижима, **честный выход —
|
||||
объявить маржу и показать её пользователю**, а не оставить леджер нижней границей молча. Выбор объяви.
|
||||
|
||||
### 4.2 Экран не врёт о единице (строка 282) — **делай РОВНО так**
|
||||
|
||||
Либо число приезжает из манифеста движка, либо подпись переименована в то, что оно на самом деле
|
||||
считает. ⛔ Третьего — «оставить как есть, все привыкли» — нет: это живая ложь на продуктовом экране.
|
||||
|
||||
### 4.3 Вход не шире выдачи (строка 325) — **реши сам, аргументируй**
|
||||
|
||||
Пока структура глав для выдачи не построена (строка 283, чужая зона), интейк обязан **отказывать**
|
||||
форматам, чью выдачу мы не умеем, — с честной причиной человеку, а не молча. Границу и формулировку
|
||||
отказа выбираешь ты; **согласуй с тем, что выдача действительно умеет сегодня**, а не с планом.
|
||||
|
||||
### 4.4 Два малых дока — **не трогай, это работа оркестратора**
|
||||
|
||||
`ENGINEERING_STANDARDS.md` и `PLATFORM_DIRECTION.md` прочитаны оркестратором 06.09 целиком: **оба живые, резать
|
||||
нечего**. `STACK_DECISIONS.md` тоже остаётся — ему добавлен указатель решений ПО ПРЕДМЕТУ (`8a9a9f4`), потому что
|
||||
ось «по сессии» заставляла искать правило перебором семи секций. ⚠ Если по ходу работы найдёшь в них утверждение,
|
||||
разошедшееся с кодом, — **пинг, не правка**: это зона оркестратора.
|
||||
|
||||
### 4.5 Чего в паке НЕТ — **не делай**
|
||||
|
||||
Не строить структуру глав для выдачи (чужая зона) · не трогать движок · не менять контракт без пинга
|
||||
оркестратору · не заводить квоты и фри-тир · не открывать бету.
|
||||
|
||||
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается
|
||||
|
||||
1. **Мутационная проверка каждого пина, который заводишь или трогаешь**: сломай охраняемое свойство,
|
||||
прогони, **прочитай ТЕКСТ падения** — говорит ли он про сломанное тобой; восстанови и **сверь
|
||||
sha256**. Артефакт: таблица «мутация → текст падения → хеш восстановлен».
|
||||
2. **Деньги — из СЫРОГО леджера ДВУМЯ независимыми путями** (норма приёмки проекта). Для `PD-441` это
|
||||
не формальность, а предмет: покажи расхождение до и после починки на реальных строках.
|
||||
3. **Опровергатель твоей готовой работы отдельным субагентом.** ⚠ Харнесс по умолчанию субагентов не
|
||||
спавнит — **тебе разрешено и требуется**. Модель задай ЯВНО (`fable`), одного-двух хватит. Мандат:
|
||||
«найди отсутствующее и противоречащее».
|
||||
4. **Греп по отозванной формулировке перед словом «исправлено»** — ноль либо только внутри пометки.
|
||||
|
||||
## 6. Оси ревью — характер «Код» и «Доки»
|
||||
|
||||
- **Код:** ⚠ «тест утверждает не то, что думает» — каждый пин обязан падать ИМЕННО от того, что он
|
||||
охраняет (§5.1).
|
||||
- **Доки:** сверка утверждений против кода и живых носителей — §4.0 и §4.4 ровно об этом.
|
||||
- **Деньги в любой приёмке — из сырого леджера двумя путями** (§5.2).
|
||||
|
||||
## 7. Записка-план ДО работы
|
||||
|
||||
≤15 строк оркестратору: что берёшь по каждому пункту · что считаешь спорным в самом заказе · как
|
||||
именно собираешься лечить `PD-441` (полная запись против объявленной маржи).
|
||||
|
||||
## 8. Заявление = команда
|
||||
|
||||
Числа и категорика — с командой и её выводом. `MAKE-EXIT` цитируй из лога, а не из кода возврата
|
||||
составной команды: смена 06.09 попалась на этом.
|
||||
|
||||
## 9. Эхо-протокол старта
|
||||
|
||||
≤10 строк ДО работы: скоуп · инварианты · не-делать.
|
||||
|
||||
## 10. Что НЕ удалось — обязательная секция
|
||||
|
||||
Не проверено · не воспроизвелось · где данных не хватило. **«Не измерено» вместо догадки.**
|
||||
|
||||
## 11. Канал вопросов и право отказаться
|
||||
|
||||
Конфликт промта с кодом — **пинг, не правка**. **Право сказать «этого делать не надо»** с аргументом:
|
||||
если по коду окажется, что предмет описан мимо, это самый ценный ответ.
|
||||
|
||||
## 12. Прямой канал
|
||||
|
||||
`CLAUDE.md` §«Связь между сессиями»: `/tmp/textmachine-channel`, свой блок первым делом, живость —
|
||||
`ListAgents`. ⚠ Роли нет или файла нет — **нормальный случай**: вопрос секцией в отчёт, сессии подряд
|
||||
не опрашивать.
|
||||
|
||||
## Деньги
|
||||
|
||||
Пак — **$0 по платным вызовам к моделям**: всё лечится на стенде и фикстурах. ⛔ Если упрёшься в «это
|
||||
предъявляется только платным прогоном» — **пинг оркестратору за санкцией, не твоё решение**. Стенд
|
||||
поднимается по рецепту в `STACK_DECISIONS.md`.
|
||||
|
||||
## Отчёт
|
||||
|
||||
Зонный журнал + сообщение оркестратору: что построено · чем предъявлено (команда и вывод) · таблица
|
||||
мутаций §5.1 · два независимых пути по деньгам §5.2 · **проход §4.0 с нулём находок без носителя** ·
|
||||
что сказал опровергатель и что ты с этим сделал · секция §10 · **что ты сам считаешь слабым местом
|
||||
своей работы**.
|
||||
|
||||
⚠ Числа снимай **после последней правки**, не до. Правки якорей объявляй наравне с содержательными.
|
||||
143
docs/POLYGON_INSTRUMENT_REPAIR_SESSION_PROMPT.md
Normal file
143
docs/POLYGON_INSTRUMENT_REPAIR_SESSION_PROMPT.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Промт: полигон-сессия «РЕМОНТ ПРИБОРА» — линза слепого чтения и гигиена стенда
|
||||
|
||||
## 1. Какая проблема и что решит твой результат
|
||||
|
||||
Мы решаем денежный вопрос: стоит ли платить за сильного редактора. Решаем замером — судьи-модели
|
||||
вслепую выбирают лучший вариант редактуры. **Прибор сегодня меряет НЕ ТО, что мы редактору заказали.**
|
||||
Судимой роли предписано «сверяй смысл черновика с исходным текстом, исправляй искажения», а первичная
|
||||
**блокирующая** линза — «точность ОТНОСИТЕЛЬНО ЧЕРНОВИКА». Редактор, правильно ушедший от черновика ради
|
||||
оригинала, по главной линзе выглядит хуже.
|
||||
|
||||
⚠ **Осторожно с масштабом — это ратифицировано так и не иначе (`D39.215` п.3):** доказано ПРОТИВОРЕЧИЕ
|
||||
КОНСТРУКЦИИ; **величина искажения НЕ ЗАМЕРЕНА**. Оркестратор сначала написал «прибор систематически
|
||||
наказывает» — и это было преувеличением: в сыром массиве такая пометка встречается ОДИН раз. Твой пак —
|
||||
не «доказать вред», а починить конструкцию и **дать величине носитель**.
|
||||
|
||||
Твой результат: следующий замер редакторского тира отвечает на вопрос, который мы задаём, а не на
|
||||
соседний.
|
||||
|
||||
⛔ **Этот пак идёт ПЕРЕД любым следующим платным замером, включая продолжение фазы Д** (промт
|
||||
`POLYGON_EXP2223_REDO_SESSION_PROMPT.md`). Причина не в очереди, а в смысле: пока линза меряет не то,
|
||||
каждый следующий замер тира стоит меньше, чем кажется.
|
||||
|
||||
## 2. Зона и git
|
||||
|
||||
**Зона записи: `eval/` + `docs/experiments/`**, пинг оркестратору — секция «Полигон» в
|
||||
`docs/PROGRESS.md`. **Сессия НЕ коммитит**, кроме пре-рег-фризов — это право полигона и единственное
|
||||
исключение. Чужие зоны не трогать. `.env` не читать.
|
||||
|
||||
## 3. Карта чтения — ЗАКОН
|
||||
|
||||
1. `CLAUDE.md` — цели и гардрейлы.
|
||||
2. Строка **319** бэклога (`docs/PROGRESS.md`, греп `^| 319 |`) — предмет целиком.
|
||||
3. `eval/dovodka/blind-read/PREREG-2-BLIND-READ.md` — §5 (линзы) и §8.
|
||||
4. `eval/dovodka/prereg-4axes/stand/editor-p9.md` — инструкция судимой роли.
|
||||
5. Строки **300**, **265**, **143** бэклога.
|
||||
|
||||
**Ратифицированное — в теле промта:**
|
||||
|
||||
- **`D39.216` (мандат владельца, 06.09), делай РОВНО так**: чинить **без хаков**; где форма не тянет —
|
||||
развязать или переписать. «Дешевле подпереть» доводом не является; отступление — **с ценой**.
|
||||
- **Комментарий/проза не доказывают поведение**; источник утверждения о смысле — прогон. ⚠ Грепом в
|
||||
`CLAUDE.md` не ищи — норма живёт нотой `D39.214`.
|
||||
- **Число называет ДЕРЕВО и ПОПУЛЯЦИЮ.** Сегодня на этом погорели три роли подряд: один и тот же счёт
|
||||
дал разные величины, потому что считали разные популяции.
|
||||
- **Рядом с нулём — контрольная величина** (`CLAUDE.md`).
|
||||
- **Эмпирика на знакомых моделям текстах — предварительная**; вес имеет замер на целевом жанре.
|
||||
|
||||
## 4. Состав и разметка свободы
|
||||
|
||||
### 4.1 Три эрраты в носителях замера — **делай РОВНО так**, первым касанием, $0
|
||||
|
||||
⛔ **Форма — БАННЕР В ШАПКЕ. Тела документов НЕ ПРАВИТЬ.** Оба заморожены, и пре-регистрация ценна
|
||||
именно неизменяемостью: правка тела уничтожает то, ради чего фриз делался.
|
||||
|
||||
1. `RESULT-2-BLIND-READ.md` — «правильный ответ по построению — ничья» противоречит собственному тексту
|
||||
того же отчёта и измеренному расхождению рук.
|
||||
2. `PREREG-2-BLIND-READ.md` §8 — выпало унаследованное ограничение «опора судьи — ЧЕРНОВИК, не
|
||||
оригинал». Верни баннером, назвав, откуда выпало.
|
||||
3. `PREREG-2-BLIND-READ.md` §5 — правило решения готово выдать «одна рука не хуже по точности» из
|
||||
НЕПРОХОЖДЕНИЯ теста, что отчёт результата запрещает дословно.
|
||||
|
||||
**Горизонт: до всего остального.** Три дешёвые правки не должны ждать дорогого пака.
|
||||
|
||||
### 4.2 Гигиена стенда (строки 300, 265) — **реши сам, аргументируй**
|
||||
|
||||
Обе помечены «до следующего платного замера»: репо-широкий сторож не дотягивается до соседнего воркри, и
|
||||
питоново зеркало держит ВТОРУЮ копию таблицы плейсхолдеров движка, уже разошедшуюся.
|
||||
|
||||
⚠ По зеркалу: **не переписывать копию, а убрать второй носитель**, если это возможно без хаков (`D39.216`);
|
||||
невозможно — предъяви почему и поставь сверку, которая краснеет при расхождении.
|
||||
|
||||
### 4.3 Линза (главное) — **делай РОВНО так в части источника, реши сам в части формы**
|
||||
|
||||
Обвязка сверки с **исходником** уже написана в этом дереве — `eval/dovodka/judge-prompts/`. **Нужно
|
||||
пере-подключение готового, а не второй прибор.**
|
||||
|
||||
Заказ: линза, судящая правку **против исходника**, плюс норматив судейства из строки **143** — **разброс
|
||||
инструмента ДО замера** и **объявленный порог различимости**. Без второго первое бессмысленно:
|
||||
односудейный вердикт при разбросе 33 % не различает ничего.
|
||||
|
||||
⛔ **Граница:** это ремонт ЗАМЕРНОГО прибора. Судейская роль внутри движка — другой предмет, не касаться.
|
||||
|
||||
### 4.4 Чего в паке НЕТ — **не делай**
|
||||
|
||||
Не продолжать фазу Д · не строить второй судейский прибор · не править тела замороженных документов ·
|
||||
не трогать движок и платформу.
|
||||
|
||||
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — механизм назван; «перечитал сам» его не удовлетворяет
|
||||
|
||||
1. **Проверка своей же починки её собственным вопросом.** Ты чинишь прибор — спроси у результата не
|
||||
«зелено ли», а **«то ли он мерит»**. Артефакт: два-три конкретных случая, где старая линза и новая
|
||||
дают РАЗНЫЙ ответ, с текстом обоих ответов.
|
||||
2. **Опровергатель отдельным субагентом.** ⚠ Харнесс по умолчанию субагентов не спавнит — **тебе
|
||||
разрешено и требуется**. Модель задай ЯВНО (`fable`), одного-двух хватит. Мандат: «найди
|
||||
отсутствующее и противоречащее», а не «проверь, всё ли хорошо».
|
||||
3. **Проверка формы эррат исполнением:** после части 4.1 — `git diff` показывает ТОЛЬКО шапки. Соблазн
|
||||
«заодно поправить тело» здесь максимальный, а цена — уничтоженный фриз.
|
||||
|
||||
## 6. Оси ревью — характер «Полигон / замеры»
|
||||
|
||||
Из таблицы приёмки: **пере-вывод чисел из СЫРЬЯ** и **независимость сигналов**. Обе твои: любое число в
|
||||
отчёте выводится из сырого массива заново, а не цитируется из прежнего отчёта.
|
||||
|
||||
## 7. Записка-план ДО работы
|
||||
|
||||
≤15 строк оркестратору: что берёшь по каждой части · какой вопрос новая линза будет задавать и какой НЕ
|
||||
будет · где ждёшь сопротивления.
|
||||
|
||||
## 8. Заявление = команда
|
||||
|
||||
Числа и категорика — с командой и её выводом. Пере-считанное из сырья — с указанием, из какого файла и
|
||||
какой популяции.
|
||||
|
||||
## 9. Эхо-протокол старта
|
||||
|
||||
≤10 строк ДО работы: скоуп · инварианты · не-делать.
|
||||
|
||||
## 10. Что НЕ удалось — обязательная секция
|
||||
|
||||
Что не проверено, что не воспроизвелось, где данных не хватило. **«Не измерено» вместо догадки.**
|
||||
|
||||
## 11. Канал вопросов и право отказаться
|
||||
|
||||
Конфликт промта с носителями — пинг, не правка. **Право сказать «этого делать не надо»** с аргументом: в
|
||||
частности, если по документам окажется, что противоречие линзы описано мимо, — это самый ценный ответ.
|
||||
|
||||
## 12. Прямой канал
|
||||
|
||||
`CLAUDE.md` §«Связь между сессиями»: `/tmp/textmachine-channel`, свой блок первым делом, живость — через
|
||||
`ListAgents`. ⚠ Роли нет или файла нет — **нормальный случай**: вопрос секцией в отчёт, сессии подряд не
|
||||
опрашивать.
|
||||
|
||||
## Деньги
|
||||
|
||||
Части 4.1 и 4.2 — **$0**. Часть 4.3 до фриза — **$0**. ⛔ **Первый платный вызов — только после фриза и
|
||||
только по санкции владельца через оркестратора. Санкции на замер этот промт НЕ несёт.** Если упрёшься в
|
||||
«это можно предъявить только платным прогоном» — пинг, а не решение.
|
||||
|
||||
## Отчёт
|
||||
|
||||
Секция «Полигон» в `docs/PROGRESS.md` + сообщение оркестратору: что построено · чем предъявлено (команда
|
||||
и вывод) · два-три случая расхождения старой и новой линзы из §5.1 · что сказал опровергатель и что ты с
|
||||
этим сделал · секция §10 · **что ты сам считаешь слабым местом своей работы**.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -22,11 +22,13 @@
|
|||
| Роль | Активный промт | Статус |
|
||||
|---|---|---|
|
||||
| Оркестратор | [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) | роль и нормы; счётчик роли — CURRENT-STATE |
|
||||
| Бэкенд · СТРУКТУРА ГЛАВ | [CHAPTER_STRUCTURE_SESSION_PROMPT.md](CHAPTER_STRUCTURE_SESSION_PROMPT.md) | **ВЫДАН 05.09** сессии `textmachine-main-fe` (`dba8a0b`). Пак 1 «форматы и честный словарь»: EPUB nav/NCX, данные структуры ПО ИСТОЧНИКУ вне пар-пака, четвёртое значение словаря `delimited` и КОНКУРЕНЦИЯ свидетелей вместо сложения путей, `title_raw`, машинная карта сдвигов. Гейт лендинга — разрез 蛊真人 байт-в-байт ЛИБО подписанная владельцем поправка с названной ценой. ⚠ Пак 2 (лексиконы не-CJK, корпус, замер) — ПОСЛЕ файлов владельца, отдельным промтом. Состав — итог восьми кругов сходимости с Fable 5 и трёх кругов ревью; тела разборов — **D39.202**, **D39.207**. |
|
||||
| **Бэкенд · КОНСИСТЕНТНОСТЬ** | [BACKEND_CONSISTENCY_SESSION_PROMPT.md](BACKEND_CONSISTENCY_SESSION_PROMPT.md) | приоритет владельца №1: канон в прогон (295) · гард перекрытия окон (267) · честный комментарий постчека (318) · не уничтожать принятую редактуру (322 а/б) · два ложных комментария. ⚠ Несёт заказ владельца на список ЛОГИЧЕСКИХ расхождений «задумано против сделано» |
|
||||
| Бэкенд · СТРУКТУРА ГЛАВ | активного НЕТ | пак 1 «форматы и честный словарь» ОТРАБОТАН, ПРИНЯТ С ДОФИКСОМ и ЗАЛЕНДЕН 06.09 — акт **D39.210**, `2f65d1d`; промт в `archive/prompts/CHAPTER_STRUCTURE_SESSION_PROMPT_2026-09-05.md` с баннером исхода. ⚠ **ПАК 2 (лексиконы не-CJK, корпус, замер оракулом, валидаторы) — НЕ НАПИСАН и ждёт файлов владельца** (страта скрейпов); промта под него нет, брать нечего. Открытые следствия пака 1 — строки бэклога **302** (резка по якорям) и **303** (безъюнитная грамматика) |
|
||||
| Бэкенд · отозванный | активного НЕТ | пак КАЧЕСТВО написан и ОТОЗВАН до выдачи 05.09 (`archive/prompts/…_WITHDRAWN.md`): предмет дешевле пака — точность чекера уже измерена, пере-снять её стоит одной команды. Следующий пак — СТРУКТУРА ГЛАВ (решение владельца 05.09 «любая книга любого формата»), пишется |
|
||||
| Бэкенд · прежний | активного НЕТ | пак «денежный стоп» ОТРАБОТАН и ПРИНЯТ С ДОФИКСОМ 05.09 — акт **D39.206** (`81a89e9` + `616a8e4`), промт в `archive/prompts/` с баннером исхода. Следующая работа зоны — строки **291** (флаг вместо клина, `D39.204`) · **294** (форма «сколько добавить», `D39.203`) · **296** (точечная перегенерация по промаху глоссария, после 295) |
|
||||
| Платформа · ФОРМА ЗАКАЗА | активного НЕТ | пак ОТРАБОТАН, ПРИНЯТ С ДОФИКСОМ и ЗАЛЕНДЕН 06.09 — акт **D39.208**, код + канон **0.11.0**; промт уведён в `archive/prompts/ORDER_FORM_SESSION_PROMPT_2026-09-05.md` с баннером исхода. ⚠ **НЕ построено, хотя заказывалось:** денежный ползунок на проводе объявлен, а отправить его нечем, и оценки под ВЫБРАННЫЙ заказ нет; «упрощение резюма» не сделано — обе позиции живут строками бэклога, брать их как новый пак СЛЕДУЮЩЕЙ сессии, не читать промт как задание |
|
||||
| Платформа · прежний | активного НЕТ | пак «пустить внутрь можно» ОТРАБОТАН и ПРИНЯТ С ДОФИКСОМ 05.09 — акт **D39.201** (`7e2226a` + `3e84716`), промт в `archive/prompts/` с баннером исхода. Следующая работа зоны — строки **294** (зеркало словаря под минор потока) ⚠ (строка 293 — зона ОРКЕСТРАТОРА, не платформы: правило игнора общее для всех рабочих деревьев клона) |
|
||||
| **Полигон · РЕМОНТ ПРИБОРА** | [POLYGON_INSTRUMENT_REPAIR_SESSION_PROMPT.md](POLYGON_INSTRUMENT_REPAIR_SESSION_PROMPT.md) | ⚠ **ИДЁТ ПЕРЕД любым следующим платным замером, включая продолжение фазы Д**: три эрраты носителей БАННЕРАМИ · гигиена стенда (300, 265) · линза против ИСХОДНИКА + порог различимости (319, 143). Санкции на платный замер НЕ несёт |
|
||||
| Полигон | [POLYGON_EXP2223_REDO_SESSION_PROMPT.md](POLYGON_EXP2223_REDO_SESSION_PROMPT.md) (отложенный — [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md), строка 85) | фаза Д ИДЁТ; ⚠ живой носитель курса — в `eval/dovodka/`, какой именно называет зона (⚠ [POLYGON_PHASE_D_HANDOFF.md](POLYGON_PHASE_D_HANDOFF.md) — перекрытый снимок, читать не как курс) |
|
||||
| Фронт | активного НЕТ | **ЗОНА ЗАМОРОЖЕНА** (D39.136 п.2 + D39.147: разморозка отдельным словом владельца, не привязана к P7); перечень первого касания — в зонном журнале |
|
||||
| Контракт | активного НЕТ | канон — `architecture/14-api-contract/`; ⚠ версию брать ФАЙЛОМ (`grep '^ version:' openapi.yaml`), не отсюда: миноры ратифицируются D-нотами и их содержание грепается по реестру нот. |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Реестр D-нот — карта актуальности v2 (D1–D39.210; титул — носитель головы, бампать при каждом аппенде)
|
||||
# Реестр D-нот — карта актуальности v2 (D1–D39.216; титул — носитель головы, бампать при каждом аппенде)
|
||||
|
||||
> ⚠ **Колонку «тело» `counts.py --check` НЕ сторожит по устройству:** он сверяет полноту НОМЕРОВ, а не
|
||||
> место тела, поэтому колонка держится дисциплиной лендинга. Не нашёл тело по колонке — иди в слайсы,
|
||||
|
|
@ -269,3 +269,9 @@
|
|||
| D39.208 | 06.09 | **АКТ: платформенный пак «форма заказа» ПРИНЯТ С ДОФИКСОМ**, залендено двумя актами (код + канон 0.11.0), гейт версии не подгоняли. Центральное: пак читал цену движка, но считал в ГЛАВАХ там, где движок считает в ЮНИТАХ — полоса символьного заказа стояла бы `0/N` навсегда. ⭐ **Норма: фикстура, в которой две РАЗНЫЕ величины совпадают, делает весь класс их расхождений невидимым** — девять находок при зелёной батарее. ⚠ Моя ошибка лендинга: pathspec молча уронил 5 untracked-файлов, включая миграцию; хук предупреждал, я обрезал его вывод пайпом | ✅ |
|
||||
| D39.209 | 06.09 | **Замер «ДО» без замера «ПОСЛЕ» ничего не доказывает: дефект, предъявленный экземпляром ВНЕ батареи, регресс-сетки не имеет по построению.** В батарее репродуцер работает сам; разрыв — там, где экземпляр за симлинком, в платном прогоне, в стенд-конфиге, в живом сценарии, в полном каталоге. Исполнимая форма: таблица «дефект → экземпляр → пере-мерен после: да/нет + КОМАНДА» в отчёте каждого пака; пропуск указывается ПОИМЁННО. Основание — ПЯТЬ экземпляров из ТРЁХ зон за неделю, ни один не пойман процессом | ✅ |
|
||||
| D39.210 | 06.09 | **АКТ: движковый пак «форматы и честный словарь» ПРИНЯТ С ДОФИКСОМ** (`2f65d1d`, 29 путей; канон не бампался — пак аддитивен). EPUB читается по `nav`/NCX, словарь получил `delimited`, пути разреза txt КОНКУРИРУЮТ; гейт 8/8 байт-в-байт, текст не потерян на девяти книгах, каталог 211. ⭐ **Норма D39.209 окупилась ДВАЖДЫ в день ратификации** — пере-замер поймал неточность отчёта и ВТОРОЙ регресс, оба экземпляром, а не тестом. ⚠ Цена на книге владельца замерена: 2187 рун оглавления внутри главы; 64 из 71 главы короче 200 знаков | ✅ |
|
||||
| D39.211 | 06.09 | **АКТ: второй круг платформенного пака** (`6ceb133` + канон `3e40c36`, `0.12.0`): заказ по размеру больше не притворяется заказом в главах — `ordered_chapters` нуллабелен, рядом `ordered_units`. ⛔ **Три лжи в каноне 0.11.0 написал Я**, и гейт не покраснил ни на одной — он сверяет ЧИСЛА, а не ФОРМЫ (строка 309). ⭐ Лечение: авторство утверждений о ЗНАЧЕНИИ отдаётся зоне с пином + порождённые примеры. ⚠ Условие Fable отклонено зоной и проверено мной по типам — буквальное исполнение было бы дефектом в деньгах | ✅ |
|
||||
| D39.212 | 06.09 | **ДОФИКС КАНОНА ЗОНОЙ ИЗ ПИНОВ** (`33b896b`, `1cae0ac`, `f05795e`, `c206067`): норма авторства исполнена первый раз и окупилась ВОПРОСОМ «а точно ли ничто иное?» — нашлась вторая форма, дающая `ceiling_chapters = 0` и неотличимая от ре-прохода на проводе; гвард стоял, но его не держал ни один тест. Плюс: `stage` как различитель отклонён (открытый словарь) · нижняя граница `0` оставлена ПОЛОМ · зеркальное расщепление в `OrderOptions.structure` · номер строки из чужого письма протухает и переносить надо ТОКЕН | ✅ |
|
||||
| D39.213 | 06.09 | **ПРИБОР ПРИЁМКИ ПЕРЕСТАЛ ВЫДАВАТЬ СПРАВКУ О НЕИЗМЕРЕННОМ** (`201363b`): попрогонный лог + гвард «лога нет ⇒ красный» + гейт формы. ⛔ Замерено, что дефект портил МОИ числа — общий лог дал призрачный пакет и СПРЯТАЛ ДВА СКИПА. ⛔ Первая редакция гейта требовала «есть выход», а не «выход КРАСНЫЙ»: ложная справка переезжала из строки для человека в КОД ВОЗВРАТА для машины — инструмент против ложной зелени сам её нёс | ✅ |
|
||||
| D39.214 | 06.09 | **«ЕСТЬ ПИН» ≠ «ПИН ДОКАЗЫВАЕТ СКАЗАННОЕ»** (`3524e47`, `49c1093`, `bf062a9`): одна трёхзначная эра протухла в ТРЁХ носителях по-разному, третьим был ЗЕЛЁНЫЙ тест — доказано отображение, сказано про жизнь. ⇒ цитируя пин, спрашивать «то ли он мерит», а не «зелен ли». Плюс: `not_started` без провенанса СПРОЕКТИРОВАН (`FinishParse`), база гейта алармов 12→14 законна как исполнение его же инструкции | ✅ |
|
||||
| D39.215 | 06.09 | **ОТЧЁТЫ КОДЕКСА: ПОЛЕЗНЫ, НО РАТИФИЦИРУЮТСЯ ПРОВЕРЕННЫЕ УТВЕРЖДЕНИЯ, А НЕ ФОРМУЛИРОВКИ** (консилиум: 40 клеймов, 31 подтверждён, 3 опровергнуты). ⛔ Центральное обвинение п.2 ОПРОВЕРГНУТО — принять его значило снять не то основание. ⭐ Сильнейший довод в отчётах ОТСУТСТВУЕТ: прибор замера штрафует поведение, которое сам заказывает. ⛔ Постчек банка молчит ровно тогда, когда сбывается названный им страх владельца. Строки 318–320 | ✅ |
|
||||
| D39.216 | 06.09 | **МАНДАТ ЧИСТОТЫ (решение владельца)**: проектировать и рефакторить без хаков и воркэраундов, где форма не тянет — переписывать; «дешевле подпереть» доводом не является; отступление объявляется С ЦЕНОЙ. ⛔ Носителя не было ВОВСЕ (ноль вхождений); `CLAUDE.md` нёс более узкое — запрет обходить НАЙДЕННУЮ ошибку. Обязан стоять строкой в каждом паке с проектированием | ✅ |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -10,7 +10,7 @@
|
|||
> ⚠ **КАК ЧИТАТЬ ШАПКУ (освежена 05.09 — оба пака 05.09 отражены в строках слоёв: `81a89e9` движковый «денежный стоп», акт D39.206; `7e2226a`+`3e84716` платформенный «пустить внутрь можно», акт D39.201).** Таблица «слой → статус → пак/D» ниже — единственный статус-борд этого файла: она БЬЁТ тело документа, а D-лог бьёт её. **Хроники лендингов паков здесь больше нет** — кто что построил и когда живёт в журнале решений: `05-decisions-log.md` + реестр `05-decisions-index.md` + слайсы `../archive/architecture/` (эры D39.100–D39.190 покрывают всю стройку платформы, шва, писателя книги и числа согласия).
|
||||
> ⚠ **ЗАКОН РАСКРЫТИЯ (D39.181) — норма зоны `backend/`, а не пункт одного пака:** число, которое движок публикует, обязано ехать со СВОИМ БАЗИСОМ, а присутствие поля не может быть булевым признаком, если оно перестало им быть. Из него же — `config_drift_basis` в `status --json` и леджер доставки в кадре `finished` (`StreamVersion` 1.2, D39.182 §4). **D39.182** («деньги и честность выдачи») добавил класс BANK в разложение денег и `noteSourceVintage`.
|
||||
> ⚠ **ПОСТРОЕНО, но в семь слоёв НЕ укладывается** (движковые оси рядом со слоями — читать шапку как полную нельзя без них): объёмный потолок оплаченной работы `--max-units`/`VolumeStop` (`backend/internal/pipeline/volume.go`, D39.170; ратифицированная единица продажи — D39.165 §1) · гейт схемы движка (`backend/internal/store/sqlgate_test.go`, D39.173: каждый SQL-оператор компилируется против схемы, поднятой продовой цепью миграций) · дверь правок банка `POST /books/{bookId}/bank/corrections` смонтирована синхронно (D39.162) · ключи деплоя доезжают до движка аргументом `--keys-file` (строка 211 ЗАКРЫТА, блокером живого прогона больше не является) · полоса прогресса СКВОЗНАЯ, подпись стадии — второе исключение границы (D39.162/163) · пере-проход можно КУПИТЬ (D39.166) · инъекция банка не теряется на провайдере с одним системным слотом (D39.164) · **ЧИСЛО СОГЛАСИЯ НА ПУТИ ВТОРОЙ ПОКУПКИ (03.09, D39.187):** пере-покупка прайсится по модели, которую стадия резолвит СЕЙЧАС, с округлением ВВЕРХ и оговоркой в тексте согласия (D39.150 п.1) — а **обещание точности снято с провода** во всех пяти местах, где стояло: контракт механизма теперь «не точность, а НАПРАВЛЕНИЕ ошибки — никогда не занизить молча»; фигура едет с базисом двумя новыми полями `status --json` (`rebill_historical_rows` · `rebill_model_moved_rows`) · **слот объёмного гранта (вторая половина строки 232):** юнит, начатый прежним прогоном и не отгруженный по оси `finalStageWave`, дописывается ВНЕ гранта и слота повторно не тратит ⚠ **следствие денежное и НЕ ратифицированное:** грант `--max-units N` оплачивает ДО 2N выходных юнитов (замер приёмки: грант 2 → четыре юнита, шесть вызовов провайдера), слово владельца 03.09 — «подумаем на этот счёт», до его решения `--max-units` держит СЛОТЫ, а деньги держит только `--ceiling-usd` (эррата 03.09-а к D39.170).
|
||||
> ⚠ **Канон контракта — 0.10.0** (D39.194, лендинг 04.09 вместе с дверью выдачи), не 0.9.0 и не 0.4.0. Носители — `docs/architecture/14-api-contract/openapi.yaml` и `platform/internal/httpapi/capabilities.go`, они сходятся гейтом.
|
||||
> ⚠ **Канон контракта — 0.12.0** (`D39.211`, лендинг 06.09: заказ по размеру перестал притворяться заказом в главах), не 0.11.0, не 0.10.0, не 0.9.0 и не 0.4.0. Носители — `docs/architecture/14-api-contract/openapi.yaml` и `platform/internal/httpapi/capabilities.go`, они сходятся гейтом.
|
||||
>
|
||||
> ⚠ **ПЛАТФОРМЕННЫЙ ШОВ — что построено 03.09 (D39.188, пак P13).** Оплаченный прогон продолжается на ТЕ деньги, за которые продан: `reopen` читает бюджет ИЗ ХОЛДА первой попытки, а прогон без холда отвечается ОШИБКОЙ, не цифрой (прежде бюджет пере-выводился по текущей ставке — `PD-168`) · **владение потоком переживает проход свипа:** чтение ОСТАНАВЛИВАЕТСЯ на чужом handshake'е (`ErrForeignStreamAhead`), потому что до этого чужие события ложились на оплаченную попытку — чужой `ceiling` ставил `paused` живому прогону, `unit_done` писал непокупленные главы (`PD-438`, major) · операторские ручки: `run unquarantine` и колонка QUARANTINE · условия хоста батареи ВЫВОДЯТСЯ из тестовых исходников (`make conditions`), а не держатся списком в прозе · два гейта в `platform/internal/gates/`: перечень условий против исходников и класс «тревожный ряд ушёл из счёта незамеченным». ⚠ **Бамп формы манифеста безопасного порядка деплоя НЕ ИМЕЕТ ни в одну сторону** (`PD-436`): гейт интейка — строгое равенство одной константе, окна двух форм нет ⇒ стоп-мир. Пока окно двух паков открыто, форма ЗАМОРОЖЕНА (D39.190 п.5).
|
||||
>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
> | 3 Контракт t/e | ✅ ПОСТРОЕН ЗА `enabled:false`: `checks.RepairCandidates` (позиционная детекция) · read-only скан остатка · машинерия ремонта §15.2; **включение платного пути = решение владельца по замеру остатка**; эмпирика контракта: диффы ОТЛОЖЕНЫ (D39.108, экспы 19/20; D21.4/D21.10 закрыты), full-regen ПОДТВЕРЖДЁН, точечная починка как ЗАМЕНА редактора отклонена и принята формой «механический пре-гейт ПЕРЕД редактором» (D39.117; строка 142, строка 106 понижена) | пак-16 (D39.24), **D39.108**, **D39.117** |
|
||||
> | 4 Память | ✅ ПОСТРОЕН + ДОСТРОЕН паком-20 + ✅ **ВХОДНАЯ ДВЕРЬ ПОСТРОЕНА (D39.158)**: $0-глагол `tmctl bank-apply` (проекция `--dry-run`, всё-или-ничего, байтовый no-op идемпотентен), писатель решений `internal/membank/decisions.go`, стоп банка — ФЛАЖОК, который движок чтит сам (память предъявления `bank_stop_presented`, схема store v16), класс отказа 15 `write_incomplete`. Роли носителей и поток «источники → вид → проекции» — `18-bank-ontology.md`; состав пака-20 (ТЕРМИНОЛОГ · флаговый стоп `--verify-bank` · двухсекционная редакторская инъекция · точечная ре-редактура · банкнота отдельным шиппинг-файлом · языковой экран `target_script`) — D39.41–45/50–53; ⚠ D39.104 п.2 ВНЕСЁН (испр. 05.09): инъекция схлопнута в ЕДИНЫЙ закон-блок, маркер ⟨проверить⟩ с провода СНЯТ — отработано вайр-батчем, пак принят D39.199 (`74ffe4f`), строка 134 закрыта; жанровый словарь ОТМЕНЁН (D39.47) | пак-1 · пак-11 · **пак-20 (D39.41–45/47/50–53)** |
|
||||
> | 5 Качество | ✅ детерм. часть: `QualityReport` · число/omission-гард · DC/пак-13-чекеры · repair-остаток; семантический span-судья/полярность — НЕ строился (защита = editor-swap, D38.4) | D39.2 · D38.3 · пак-13 (`2f91b04`) · пак-16 |
|
||||
> | 6 Export | ✅ ДВИЖКОВАЯ половина ПОСТРОЕНА: `checks.ExportNormalize` на каждом финале; санитайзер-классы strip+export · **писатель книги: `internal/bookfile` + `tmctl build` (EPUB 3 + txt, D39.175)**; холодный прогон 31.08 предъявил собранный EPUB настоящего перевода, `epubcheck` 0/0/0/0. ✅ **ПЛАТФОРМЕННАЯ половина ПОСТРОЕНА 04.09, ЦИКЛ ЗАМКНУТ ЖИВЬЁМ** (`adf5e53`, D39.194): пользователь получил EPUB настоящего ПЛАТНОГО перевода ЧЕРЕЗ API, `epubcheck` 5.3.0 на СКАЧАННОМ файле 0/0/0/0. Дверь — три адреса (`createExport`/`getExport`/`downloadExport`), `202`+`Location`, поллинг с `Retry-After`, Range 206, TTL с уборкой, идемпотентность. Проверка вместо счёта: `grep -rn 'createExport\|getExport' platform/internal/ --include=*.go` → НЕ пусто; операций в каноне 21, смонтировано **19** (испр. 05.09: `updateBook` смонтирован платформенным паком `7e2226a`). ⚠ **СТЕНА ДЕСЯТИ ГЛАВ СНЯТА 05.09, НО ПОЛ ОСТАЛСЯ — ДОЧИТАТЬ ДО КОНЦА ПО-ПРЕЖНЕМУ НЕЛЬЗЯ.** — движковый пак `81a89e9` (акт D39.206) снял стену: отказ резервации ЗАЩЁЛКИВАЕТ волну, а обязательный вызов ЖДЁТ settle; порог «короче десяти глав не переводится» СНЯТ, остаточный пол — одна резервация (~3 главы) и он ВЫВЕДЕН, не замерен. Разбор — `15-money-path.md`, греп `ЭРРАТА 05.09`. ⛔ **Инвариантный пол: даже при ОДНОМ воркере прирост потолка `2 × $0.03 = $0.06` меньше одной редакторской резервации (~$0.07), то есть ПОСЛЕДНИЕ ~2 ГЛАВЫ любой книги не покупаются** (`15-money-path.md`, греп `даже при ОДНОМ воркере`). Защёлка тут не помогает — она делает стоп штатным, а не финансирует последний вызов. Снимается ПЛАТФОРМЕННОЙ половиной (потолок ДЕНЬГАМИ, `D39.196`, строка 280), которая НЕ построена: `git grep -lE "StepMax|step_max" -- platform/` → пусто. ⚠ **Пол ВЫВЕДЕН, не замерен** (платного прогона не было — D39.206 п.6). ⇒ `PD-440` жив в ЭТОЙ половине: его волновая часть починена движком, денежная — нет | D39.2 · D38.3 · D39.175 · D39.178 |
|
||||
> | 6 Export | ✅ **ПЛАТФОРМЕННАЯ половина ПОСТРОЕНА 06.09** — что именно и с какой ценой, ОДНИМ носителем: `15-money-path.md` §3 (акт `D39.208`). Здесь только статус, механика не дублируется. ✅ ДВИЖКОВАЯ половина ПОСТРОЕНА: `checks.ExportNormalize` на каждом финале; санитайзер-классы strip+export · **писатель книги: `internal/bookfile` + `tmctl build` (EPUB 3 + txt, D39.175)**; холодный прогон 31.08 предъявил собранный EPUB настоящего перевода, `epubcheck` 0/0/0/0. ✅ **ПЛАТФОРМЕННАЯ половина ПОСТРОЕНА 04.09, ЦИКЛ ЗАМКНУТ ЖИВЬЁМ** (`adf5e53`, D39.194): пользователь получил EPUB настоящего ПЛАТНОГО перевода ЧЕРЕЗ API, `epubcheck` 5.3.0 на СКАЧАННОМ файле 0/0/0/0. Дверь — три адреса (`createExport`/`getExport`/`downloadExport`), `202`+`Location`, поллинг с `Retry-After`, Range 206, TTL с уборкой, идемпотентность. Проверка вместо счёта: `grep -rn 'createExport\|getExport' platform/internal/ --include=*.go` → НЕ пусто; операций в каноне 21, смонтировано **19** (испр. 05.09: `updateBook` смонтирован платформенным паком `7e2226a`). ⚠ **СТЕНА ДЕСЯТИ ГЛАВ СНЯТА 05.09, НО ПОЛ ОСТАЛСЯ — ДОЧИТАТЬ ДО КОНЦА ПО-ПРЕЖНЕМУ НЕЛЬЗЯ.** — движковый пак `81a89e9` (акт D39.206) снял стену: отказ резервации ЗАЩЁЛКИВАЕТ волну, а обязательный вызов ЖДЁТ settle; порог «короче десяти глав не переводится» СНЯТ, остаточный пол — одна резервация (~3 главы) и он ВЫВЕДЕН, не замерен. Разбор — `15-money-path.md`, греп `ЭРРАТА 05.09`. ⛔ **Инвариантный пол: даже при ОДНОМ воркере прирост потолка `2 × $0.03 = $0.06` меньше одной редакторской резервации (~$0.07), то есть ПОСЛЕДНИЕ ~2 ГЛАВЫ любой книги не покупаются** (`15-money-path.md`, греп `даже при ОДНОМ воркере`). Защёлка тут не помогает — она делает стоп штатным, а не финансирует последний вызов. Снимается ПЛАТФОРМЕННОЙ половиной (потолок ДЕНЬГАМИ, `D39.196`, строка 280), которая НЕ построена: `git grep -lE "StepMax|step_max" -- platform/` → пусто. ⚠ **Пол ВЫВЕДЕН, не замерен** (платного прогона не было — D39.206 п.6). ⇒ `PD-440` жив в ЭТОЙ половине: его волновая часть починена движком, денежная — нет | D39.2 · D38.3 · D39.175 · D39.178 |
|
||||
> | 7 Гигиена | ✅ ПОСТРОЕН: реестр role→инъекция · вынос `foldModelWire` · снос сирот/`gatesEnabled` · **target-aware гейты ПО ДАННЫМ — П1 «цель-шов» (фаза 2, D39.64: `isRuTarget` удалён, `TargetActive`/`TargetScriptNonLatin`, токенизатор цели из `word_script`); остаток — ё-фолд/segmentation за подписью (бэклог-строка 14)** | D39.2 · паки 11–16 · **фаза 2 (D39.64)** |
|
||||
>
|
||||
> **Общность (фаза 2) ИСПОЛНЕНА И ПРИНЯТА — D39.64**, отчёт `../archive/reports/GENERALITY_PHASE2_2026-08-01.md`: П0 хеш эмбед-данных (`embedded_version` в снапшоте) · П1 цель-шов (слой 7) · П2 скрипт-шов источника (`lang-script.txt`) · П3 нарезка/оценка (один дом `DenseScript`) · П4 манифест-по-каналам (`packAlgoVersion` не бампался); голден вердикт-нейтрален (0/172); П5 майнер гейтится второй книгой; 9 флагов §5 — очередь владельца (бэклог-строка 14). **Карта языковой привязки ВСЕГО движка — `../archive/reports/GENERALITY_RESEARCH_2026-07-31.md` (D39.60)**; пак-19 «голос/состояние» — D39.54–56.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ openapi: 3.1.0
|
|||
|
||||
info:
|
||||
title: TextMachine API
|
||||
version: 0.11.0
|
||||
version: 0.12.0
|
||||
summary: Ratified contract between the frontend and the TextMachine platform.
|
||||
description: |
|
||||
**RATIFIED contract.** Canonical copy: `docs/architecture/14-api-contract/`;
|
||||
|
|
@ -1446,8 +1446,45 @@ components:
|
|||
Progress:
|
||||
type: object
|
||||
description: |
|
||||
How far THIS RUN has got through the whole of its own work, in chapters, as ONE monotonic
|
||||
fraction. A run may make more than one pass over the chapters it bought, and it may stop
|
||||
How far THIS RUN has got through the whole of its own work, as ONE monotonic fraction.
|
||||
|
||||
⚠ **THE UNIT FOLLOWS HOW THE ORDER WAS PHRASED, and it is read off `Run.ordered_chapters`
|
||||
and `Run.ordered_units` — the pair that describes the ORDER — in this order, first match
|
||||
wins:**
|
||||
|
||||
1. **`ordered_chapters == 0` ⇒ a RE-PASS.** `total` is `1` and `done` is `0` until it
|
||||
finishes clean, then `1`: a declared shape, not a count of anything. Do not render it as
|
||||
"0 of 1 chapters". Held by `pgstore.TestARePassRunsBarIsOneUnitOfWork`, and its
|
||||
EXCLUSIVITY — that nothing else can put a zero here — by
|
||||
`runs.TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun`: a whole-book order
|
||||
over a fully delivered book quotes zero chapters, and the service refuses it rather than
|
||||
admitting a run that would wear the re-pass's shape without being one.
|
||||
2. **`ordered_chapters == null` ⇒ the order was placed in CHARACTERS**, and the counters are
|
||||
the engine's own shipping units — the same `Unit` this contract already carries. The size
|
||||
bought stands in `ordered_units`. Held by
|
||||
`runs.TestACharacterOrdersBarIsCountedInWhatItActuallyBought` and
|
||||
`runs.TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`.
|
||||
3. **Otherwise (`ordered_chapters >= 1`) ⇒ the counters are CHAPTERS.** Held by
|
||||
`runs.TestAChapterOrderKeepsTheBarItAlwaysHad`.
|
||||
|
||||
⛔ **THE ORDER OF THOSE CHECKS IS LOAD-BEARING, and `Run.delivered_chapters` is NOT the
|
||||
discriminator** — an earlier edition of this paragraph said it was, and it was wrong in the
|
||||
one case that matters. A re-pass answers `delivered_chapters: 0`, which is a NUMBER, so the
|
||||
rule "a number means chapters" sends a client to case 3 and it draws the very "0 of 1
|
||||
chapters" this paragraph forbids. `delivered_chapters: null` does mark case 2 — it is derived
|
||||
from the same fact — but it cannot separate case 1 from case 3, so a client that starts there
|
||||
gets a re-pass wrong. Measured, not reasoned: the re-pass row of
|
||||
`pgstore.TestARePassRunsBarIsOneUnitOfWork` asserts `delivered_chapters: 0` beside
|
||||
`ordered_chapters: 0`.
|
||||
|
||||
⚠ **Do not key on `stage` either.** `stage: re_pass` is true of case 1 today, but `stage` is
|
||||
an OPEN vocabulary whose new values move no version — a client owes it degradation, not
|
||||
decisions. It corroborates; it does not discriminate.
|
||||
|
||||
**Do not render a unit noun without running the three checks** — "3 of 10 chapters" drawn
|
||||
over units is a lie the client tells on our behalf. A client that draws a PERCENTAGE needs
|
||||
none of this: the fraction is meaningful in every case, which is why no ready-made percentage
|
||||
is shipped. A run may make more than one pass over the chapters it bought, and it may stop
|
||||
part-way for the book's terms to be signed; the counter spans all of that. **It never
|
||||
restarts from zero** — not when a signing stop is cleared, not when one pass gives way to the
|
||||
next. The counters only grow and nothing under them moves.
|
||||
|
|
@ -1711,12 +1748,35 @@ components:
|
|||
- type: string
|
||||
- type: 'null'
|
||||
description: |
|
||||
Where this book's chapter boundaries came from, passed through from the engine's
|
||||
manifest; `null` while the book is still arriving. ⚠ **Added by the order-form minor
|
||||
(0.11.0)** so a client can tell an order in chapters from an order it must phrase in
|
||||
characters.
|
||||
Where this book's chapter boundaries came from, in the engine's own word, passed through
|
||||
untouched — or `null` when NO MANIFEST HAS BEEN READ FOR THIS BOOK. ⚠ **Added by the
|
||||
order-form minor (0.11.0)** so a client can tell an order in chapters from an order it
|
||||
must phrase in characters.
|
||||
|
||||
⚠ **An OPEN vocabulary that grows additively** — today `declared`, `detected`, `none`.
|
||||
⛔ **`null` IS NOT A VALUE OF THE VOCABULARY AND IT IS NOT `none`.** `none` is a book the
|
||||
engine LOOKED at and found one chapter in — a statement about the book. `null` is the
|
||||
absence of any statement: nobody has asked. The difference is the whole reason a client
|
||||
can tell «there is no provenance» from «there is one, and it is poor», and only the
|
||||
second is a fact about the text.
|
||||
|
||||
⚠ **And `null` does NOT mean «still arriving».** An earlier edition of this line said so,
|
||||
and it is narrower than the truth. A book reaches `not_started` the moment its parse
|
||||
finishes — and that same transaction leaves it OWING a reading surface, which is what
|
||||
later brings the provenance. So the ordinary state of a freshly parsed book is `cut, and
|
||||
no provenance yet`, and a client that waits for arrival waits for a thing that already
|
||||
happened. Held by `pgstore.TestAParsedBookIsNotStartedBeforeItHasAnyProvenance`.
|
||||
|
||||
⚠ Two further roads reach the same `null`, and neither is a book in flight: a manifest
|
||||
older than this field leaves the column empty for good, and a book registered from a file
|
||||
already on disk has no manifest at all yet. ⇒ `not_started` says the SOURCE was cut; it
|
||||
does not say a manifest has been read.
|
||||
|
||||
The rendering half — that an absent provenance reaches the client as an explicit `null`
|
||||
rather than a missing member or an invented word, and that a word this build does not
|
||||
know travels VERBATIM — is held separately by
|
||||
`httpapi.TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull`.
|
||||
|
||||
⚠ **An OPEN vocabulary that grows additively** — today `declared`, `delimited`, `detected`, `none` (⚠ `delimited` added by the engine 06.09: the file drew a separator — an EPUB spine, a form feed — but calling it a CHAPTER is the engine's assumption, not the format's word).
|
||||
A value this build does not know is read as "not recognised" and is never promoted into
|
||||
trust. A client MUST do the same: switch on the values it knows and treat the rest as
|
||||
"no usable structure", rather than compile an exhaustive list that a minor will outgrow.
|
||||
|
|
@ -1809,13 +1869,31 @@ components:
|
|||
run that was asked to continue is no longer a run someone asked to stop. A refused
|
||||
`resume` clears nothing — nothing was withdrawn.
|
||||
ordered_chapters:
|
||||
type: integer
|
||||
type: [integer, 'null']
|
||||
minimum: 0
|
||||
description: |
|
||||
⚠ **`0` is legal and is not "nothing was ordered":** a re-pass buys no chapters, and an
|
||||
order phrased in CHARACTERS spans no whole chapter — both report `0` here while being
|
||||
perfectly ordinary runs. A client that treats this as at-least-one will reject valid
|
||||
answers.
|
||||
⚠ **`null` when the order was NOT placed in chapters** — an order phrased in
|
||||
characters carries its size in `ordered_units` instead. **Exactly one of the two is
|
||||
filled**, and which one says how the buyer expressed themselves.
|
||||
|
||||
⚠ **`0` is legal and means a re-pass:** it buys no chapters while being a perfectly
|
||||
ordinary run. A client that treats this field as at-least-one will reject valid answers.
|
||||
|
||||
⚠ **The bound was `minimum: 1` until this was measured, and it contradicted the line
|
||||
above it** — a generated validating client would have rejected every re-pass. `null` was
|
||||
never at risk (a bound constrains numbers, not the null branch); the value it excluded
|
||||
was the zero. Held by `pgstore.TestARePassRunsBarIsOneUnitOfWork`, which asserts `0` on
|
||||
BOTH surfaces a run is handed out on — the start receipt and the read path — because they
|
||||
are two different expressions and a promise made once must hold on both.
|
||||
|
||||
The bound is kept rather than dropped: `0` is the floor and not merely today's smallest
|
||||
value. A negative is refused at admission by the service itself, so the schema says out
|
||||
loud what the store already guarantees.
|
||||
|
||||
⚠ **Changed in 0.12.0.** Until then this field carried, for a character order, the SPAN
|
||||
of chapters the order reached into — a number that could exceed what was bought (an order
|
||||
of one unit out of four reported `1`). That was a defect: the field is named for what was
|
||||
ORDERED. It is now `null` there, and the honest size stands beside it.
|
||||
|
||||
What this run was ORDERED to deliver, in chapters — a property of the RUN. Present so a
|
||||
reloaded screen can name what the user bought and read `progress` against it.
|
||||
|
|
@ -1824,15 +1902,35 @@ components:
|
|||
"limit" when the figure is a PURCHASE: a ceiling is what a run may not exceed, an order is
|
||||
what it owes. The retired name is rejected on the way IN with a pointer to `/chapters`,
|
||||
never silently reinterpreted.
|
||||
ordered_units:
|
||||
oneOf:
|
||||
- type: integer
|
||||
minimum: 1
|
||||
- type: 'null'
|
||||
description: |
|
||||
What this run was ordered to deliver when the order was placed in CHARACTERS, counted in
|
||||
the engine's own shipping units — the same `Unit` this contract already carries. `null`
|
||||
for an order placed in chapters. ⚠ **Added in 0.12.0.**
|
||||
|
||||
⚠ **This is the unit of our ANSWER, not of the buyer's QUESTION.** They asked in
|
||||
characters; the number they typed is not stored anywhere today, so a reloaded screen can
|
||||
say "5 units" but not "4500 characters". Whether that echo is owed is a product question,
|
||||
not a contract one.
|
||||
delivered_chapters:
|
||||
oneOf:
|
||||
- type: integer
|
||||
minimum: 0
|
||||
- type: 'null'
|
||||
description: |
|
||||
Whole chapters this run has delivered, or `null` when the question does not apply —
|
||||
an order expressed in CHARACTERS stops inside a chapter and closes none, and `0` there
|
||||
would read as "nothing happened" when work was in fact done and paid for.
|
||||
Whole chapters this run has delivered, or `null` when the question does not apply.
|
||||
`0` would read as "nothing happened" when work was in fact done and paid for.
|
||||
|
||||
⚠ **The `null` is a POLICY, not an accident of arithmetic.** An order expressed in
|
||||
characters is measured in characters throughout, and landing flush with a chapter
|
||||
boundary does not change the unit it was bought in — on a book whose chapters are one
|
||||
unit each, every such order closes whole chapters and still reports `null`. Two purchases
|
||||
a person cannot tell apart must not be drawn differently. ⚠ A previous edition justified
|
||||
this by claiming such an order "closes no whole chapter"; that was false and is withdrawn.
|
||||
term_consistency_funded:
|
||||
type: boolean
|
||||
description: |
|
||||
|
|
@ -2562,9 +2660,14 @@ components:
|
|||
Order by size instead of by chapters, for a book whose chapter boundaries this build
|
||||
does not trust — `order.chapter_orders: false` says when that is the case.
|
||||
|
||||
⚠ **Such an order stops INSIDE a chapter**, so the run it starts reports
|
||||
`delivered_chapters: null` rather than `0`: it closed no whole chapter, and `0` would
|
||||
read as "nothing happened" when work was done and paid for.
|
||||
⚠ **The run it starts reports `delivered_chapters: null` rather than `0`, and the
|
||||
reason is a POLICY, not an accident of arithmetic:** an order expressed in characters is
|
||||
measured in characters throughout, and landing flush with a chapter boundary does not
|
||||
change the unit it was bought in. ⚠ A previous edition justified the `null` by claiming
|
||||
such an order "closes no whole chapter" — **false**: whether it closes one depends on
|
||||
where the unit boundary fell, and on a book of one-unit chapters every such order closes
|
||||
them. The `null` holds regardless, which is the point: two purchases a person cannot tell
|
||||
apart must not be drawn differently.
|
||||
|
||||
Mutually exclusive with `chapters`; a request carrying both is `400`.
|
||||
ceiling_chapters:
|
||||
|
|
@ -2656,8 +2759,22 @@ components:
|
|||
- type: string
|
||||
- type: 'null'
|
||||
description: |
|
||||
Where the chapter boundaries came from, passed through from the engine's manifest.
|
||||
⚠ **An open vocabulary that GROWS additively** — today `declared`, `detected`, `none`.
|
||||
Where the chapter boundaries came from, passed through from the engine's manifest, or
|
||||
`null` when NO MANIFEST HAS ANSWERED YET.
|
||||
|
||||
⚠ **`null` is not a KIND of cut at all, and it is not `none`.** (⚠ Said "a fourth kind" until 06.09, when the vocabulary already held four values and `null` would have been a fifth: the sentence was written in the three-value era and the count went stale under it. The number is gone rather than corrected, so the rule outlives the next value too.) `none` is a book the engine
|
||||
looked at and found one chapter in; `null` is a book it has not been asked about. Having no
|
||||
provenance and having a poor one are different facts, and only the second says anything
|
||||
about the book. Held by
|
||||
`httpapi.TestABookWhoseCutCannotBeSoldAgainstOffersCharactersAndSaysSo`, which asserts an
|
||||
explicit `null` rather than an invented word. ⚠ The sibling `Book.structure` carries the
|
||||
same fact and now says the same in its own words — until 06.09 it did NOT: it called
|
||||
`null` «still arriving» and drew no line between `null` and `none` at all, so the rule
|
||||
lived in the ORDER schema while the field a client reads on the book card was silent
|
||||
about it. «The two cannot disagree» was a statement of intent then; it is a statement
|
||||
about the text now.
|
||||
|
||||
⚠ **An open vocabulary that GROWS additively** — today `declared`, `delimited`, `detected`, `none` (⚠ `delimited` added by the engine 06.09: the file drew a separator — an EPUB spine, a form feed — but calling it a CHAPTER is the engine's assumption, not the format's word).
|
||||
A value this build does not know is read as "not recognised" and never promoted into
|
||||
trust; a client MUST do the same rather than switch on an exhaustive list.
|
||||
source_chars:
|
||||
|
|
|
|||
|
|
@ -29,10 +29,21 @@
|
|||
## 3. Деньги ПЛАТФОРМЫ по шагам (зона `platform/`)
|
||||
|
||||
1. **Грант.** `Grant` — строка леджера `kind='grant'`, идемпотентная по `(аккаунт, source, source_id)`: `on conflict (user_id, source, source_id) do nothing` (`platform/internal/pgstore/credits.go`, греп `func (s *Store) Grant(`; сама идемпотентность после пака sqlc уехала В ЗАПРОС — `platform/internal/pgstore/queries/credits.sql`, греп `on conflict (user_id, source, source_id) do nothing`). Фри-тир — тот же механизм в ТОЙ ЖЕ транзакции, что создание аккаунта (`platform/internal/pgstore/identity.go`, греп `appendLedger(ctx, tx, userID, "grant"`; ⚠ **дефолт `SignupGrantMicroUSD` — НОЛЬ** (`platform/internal/config/config.go`, греп `SignupGrantMicroUSD:`), а НЕ «5 × 1_000_000» — эта сумма ходила по докам и ложна: предложение «на бете дефолт в НОЛЬ» (D39.110 п.3) ИСПОЛНЕНО, `PD-104` в регистре стоит `fixed(P7, дерево сессии)`).
|
||||
2. **Выбор потолка — шкала в ГЛАВАХ** (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в `book.yaml` не пишется). **Максимум = Balance КАК ЕСТЬ** (D39.115 п.2а; эррата в шапке D-лога — `docs/architecture/05-decisions-log.md:4`=`Balance КАК ЕСТЬ`): `Hold` — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: `pricing.Model.Scale` — `affordable = balance / PerChapter`, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — `platform/internal/pricing/pricing.go`, греп `func (m Model) Scale(`.
|
||||
3. **Ставка $0.03/глава — конфиг-константа платформы:** `DefaultPerChapter = 30_000` micro-USD (`platform/internal/pricing/pricing.go`, греп `DefaultPerChapter`), переопределяется `PerChapterMicroUSD` (`platform/internal/config/config.go`, греп `PerChapterMicroUSD`). ⚠⚠ **ЧИСЛА НИЖЕ — ИЮЛЬСКИЕ ДЕНЬГИ И С 16.08 НЕ ДЕЙСТВУЮТ (испр. ревизией 02.09).** D39.179 п.1 ратифицировал: DeepSeek пере-пинен под цены, вступившие **16.08 16:00 UTC**, множитель к июльским деньгам **×4.47**. ⇒ по действующему прайсу та же выборка даёт среднюю главу **≈$0.051–$0.085** и максимум **≈$0.168**, то есть константа $0.03 не завышена, а **ЗАНИЖЕНА**, и знак вывода перевернулся. ⚠ Иллюстрация живыми деньгами (НЕ калибровка): холодный прогон 31.08 дал полную цепь главы $0.093 и $0.151 при `projected_book_usd = 1.1377277707` — но это **n=2 главы из десяти**, прогон остановлен снапшот-гардом, промпт черновика был банкнотным вместо конвенционного, и сам отчёт запрещает такое употребление дословно (`archive/reports/COLDRUN_V16_REPORT_2026-08-31.md`, греп `калибровать ставку платформы`). **Провенанс ниже — ИСТОРИЯ (D39.165 §1, 28.08):** средняя **$0.0115–$0.0190**, максимум одной главы $0.0375, «завышена в **1.6–2.6×**» — ⚠ но НЕ покрывает максимум: источник (D39.165 §1) добавляет «и отдельные главы её уже пробивают» ($0.0375 против $0.03 = ×1.25), и «купить 10 глав» отдаёт движку $0.30, покупающий ~16–26 глав («денежная ручка с подписью „главы"», `PD-410`). РЕШЕНО: цена — от ОБЪЁМА ИСХОДНИКА (носитель `chapters.units_total`, пишется интейком за $0), настоящий стоп — объёмный потолок В ДВИЖКЕ (см. §2). ⛔ Константу по сегодняшним числам НЕ калибровать (D39.165 ⛔г) — ⚠ **испр. 05.09: гейт ПАЛ вместе со строкой 202** — живой платный прогон отработал 04.09 и показал, что ставка занижена ×4.47; строка 202 закрыта D39.200. Калибровка ставки идёт заказом строки **281** (⚠ испр.: строка 278 ЗАКРЫТА лендингом `81a89e9` и снята — проекция цены построена и опубликована) (проекция цены и замер стоимости тысячи знаков), а не запретом. Исторический провенанс: exp08 v2 $0.0219/глава → ревизия D30.4 +15–25% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; `platform/internal/pricing/pricing.go`, греп `Provenance, so the number is auditable`). ⚠ **испр. 05.09: движковая половина ПОСТРОЕНА и опубликована** — `expected_usd` по главам, `book_once_usd`, `step_max_usd` в `manifest --json`/`status --json` (лендинг `81a89e9`, акт D39.206). Непостроенной осталась ПЛАТФОРМЕННАЯ половина: `git grep -lE "StepMax|step_max" -- platform/` пусто, то есть числа публикуются и никем не читаются.
|
||||
2. ⚠⚠ **ИСПР. 06.09 — ШКАЛЫ В ГЛАВАХ БОЛЬШЕ НЕТ (лендинг формы заказа, `D39.208`).** Объём заказывают
|
||||
ТРЕМЯ формами: вся книга (дефолт) · до главы N · знаками; ползунок — ДЕНЕЖНЫЙ (`limit.min/max_micro_usd`,
|
||||
минимум = неделимый шаг движка, и он может быть БОЛЬШЕ максимума, когда баланс не несёт даже одной
|
||||
резервации). Ниже — прежняя редакция, историей: **Выбор потолка — шкала в ГЛАВАХ** (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в `book.yaml` не пишется). **Максимум = Balance КАК ЕСТЬ** (D39.115 п.2а; эррата в шапке D-лога — `docs/architecture/05-decisions-log.md:4`=`Balance КАК ЕСТЬ`): `Hold` — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: `pricing.Model.Scale` — `affordable = balance / PerChapter`, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — `platform/internal/pricing/pricing.go`, греп `func (m Model) Scale(`.
|
||||
3. ⛔⛔ **СТАВКИ ЗА ГЛАВУ БОЛЬШЕ НЕ СУЩЕСТВУЕТ — ни константы, ни ручки (испр. 06.09).** Цена берётся из
|
||||
ПРОЕКЦИИ ДВИЖКА (`expected_usd` · `book_once_usd` · `step_max_usd` в манифесте), холд считается
|
||||
`k × ожидаемое + шаг [+ книжный бонд]`. `PerChapterMicroUSD` удалён целиком; от `DefaultPerChapter`
|
||||
в дереве остался ОДИН хит — надгробный комментарий `platform/internal/pricing/pricing.go` («Until
|
||||
05.09 a chapter was worth a CONSTANT»), и грепать его как живой носитель нельзя. Прежняя редакция
|
||||
историей: **Ставка $0.03/глава — конфиг-константа платформы:** `DefaultPerChapter = 30_000` micro-USD (`platform/internal/pricing/pricing.go`, греп `DefaultPerChapter`), переопределяется `PerChapterMicroUSD` (`platform/internal/config/config.go`, греп `PerChapterMicroUSD`). ⚠⚠ **ЧИСЛА НИЖЕ — ИЮЛЬСКИЕ ДЕНЬГИ И С 16.08 НЕ ДЕЙСТВУЮТ (испр. ревизией 02.09).** D39.179 п.1 ратифицировал: DeepSeek пере-пинен под цены, вступившие **16.08 16:00 UTC**, множитель к июльским деньгам **×4.47**. ⇒ по действующему прайсу та же выборка даёт среднюю главу **≈$0.051–$0.085** и максимум **≈$0.168**, то есть константа $0.03 не завышена, а **ЗАНИЖЕНА**, и знак вывода перевернулся. ⚠ Иллюстрация живыми деньгами (НЕ калибровка): холодный прогон 31.08 дал полную цепь главы $0.093 и $0.151 при `projected_book_usd = 1.1377277707` — но это **n=2 главы из десяти**, прогон остановлен снапшот-гардом, промпт черновика был банкнотным вместо конвенционного, и сам отчёт запрещает такое употребление дословно (`archive/reports/COLDRUN_V16_REPORT_2026-08-31.md`, греп `калибровать ставку платформы`). **Провенанс ниже — ИСТОРИЯ (D39.165 §1, 28.08):** средняя **$0.0115–$0.0190**, максимум одной главы $0.0375, «завышена в **1.6–2.6×**» — ⚠ но НЕ покрывает максимум: источник (D39.165 §1) добавляет «и отдельные главы её уже пробивают» ($0.0375 против $0.03 = ×1.25), и «купить 10 глав» отдаёт движку $0.30, покупающий ~16–26 глав («денежная ручка с подписью „главы"», `PD-410`). РЕШЕНО: цена — от ОБЪЁМА ИСХОДНИКА (носитель `chapters.units_total`, пишется интейком за $0), настоящий стоп — объёмный потолок В ДВИЖКЕ (см. §2). ⛔ Константу по сегодняшним числам НЕ калибровать (D39.165 ⛔г) — ⚠ **испр. 05.09: гейт ПАЛ вместе со строкой 202** — живой платный прогон отработал 04.09 и показал, что ставка занижена ×4.47; строка 202 закрыта D39.200. Калибровка ставки идёт заказом строки **281** (⚠ испр.: строка 278 ЗАКРЫТА лендингом `81a89e9` и снята — проекция цены построена и опубликована) (проекция цены и замер стоимости тысячи знаков), а не запретом. Исторический провенанс: exp08 v2 $0.0219/глава → ревизия D30.4 +15–25% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; `platform/internal/pricing/pricing.go`, греп `Provenance, so the number is auditable`). ⚠ **испр. 05.09: движковая половина ПОСТРОЕНА и опубликована** — `expected_usd` по главам, `book_once_usd`, `step_max_usd` в `manifest --json`/`status --json` (лендинг `81a89e9`, акт D39.206). Непостроенной осталась ПЛАТФОРМЕННАЯ половина: `git grep -lE "StepMax|step_max" -- platform/` пусто, то есть числа публикуются и никем не читаются.
|
||||
4. **Холд прироста ДО спавна.** `holdTx` пишет отрицательную строку `('hold', -amount)` и тем же знаком двигает кэш баланса (`platform/internal/pgstore/credits.go`, греп `func holdTx(` — строку леджера пишет `appendLedger(… "hold", -amount …)`, кэш баланса двигает `MoveBalance` внутри неё); нехватка — `ErrInsufficientCredit`. Холд + прогон + попытка + очередь — одна транзакция (`platform/internal/pgstore/runs.go`, греп `func (s *Store) StartRun(`; рестарт — греп `func (s *Store) RestartRun(`; форма D39.123 п.2д).
|
||||
5. **Аргумент движку: `bookCap = committed + прирост×ставка`, БЕЗ reserved — PD-158, дословно из кода: `return m.committed + increment`** (`platform/internal/runs/spawn.go`, греп `return m.committed + increment`). Обе фигуры читаются ОДНИМ вызовом `status --json` перед стартом (`bookMeter` — `platform/internal/runs/spawn.go`, греп `func (s *Service) bookMeter(`; отказы — греп `Absent is not zero`); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (`docs/glossary.md:55`=`ЭТО НЕ ХОЛД — он больше холда`). Фактически ушедшее значение хранится в `run_attempts.ceiling_arg_micro_usd` (PD-144, миграция 00011); потолок уходит argv-шаблоном `{{usd}}` (шаблон — `platform/internal/config/config.go`, греп `CeilingArg`; подстановку делает `platform/internal/runner/engine.go`, греп `{{usd}}`; подстановку и рендер argv делает `platform/internal/runner/engine.go`, греп `func (t CeilingTemplate) Args(`; `platform/internal/runs/runs.go`, греп `func (s *Service) ceilingFor(` — однострочная делегация к нему; `spawn.go` потолок только передаёт); строка 145 закрыта D39.122.
|
||||
5. ⚠ **ИСПР. 06.09: формула ЖИВА дословно, сменился ИСТОЧНИК прироста.** `return m.committed + increment`
|
||||
стоит на месте (`platform/internal/runs/spawn.go`, греп `func (m meter) bookCap`), но `increment`
|
||||
приходит теперь из проекции движка, а не из «глав × ставку». Прежняя редакция историей:
|
||||
**Аргумент движку: `bookCap = committed + прирост×ставка`, БЕЗ reserved — PD-158, дословно из кода: `return m.committed + increment`** (`platform/internal/runs/spawn.go`, греп `return m.committed + increment`). Обе фигуры читаются ОДНИМ вызовом `status --json` перед стартом (`bookMeter` — `platform/internal/runs/spawn.go`, греп `func (s *Service) bookMeter(`; отказы — греп `Absent is not zero`); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (`docs/glossary.md:55`=`ЭТО НЕ ХОЛД — он больше холда`). Фактически ушедшее значение хранится в `run_attempts.ceiling_arg_micro_usd` (PD-144, миграция 00011); потолок уходит argv-шаблоном `{{usd}}` (шаблон — `platform/internal/config/config.go`, греп `CeilingArg`; подстановку делает `platform/internal/runner/engine.go`, греп `{{usd}}`; подстановку и рендер argv делает `platform/internal/runner/engine.go`, греп `func (t CeilingTemplate) Args(`; `platform/internal/runs/runs.go`, греп `func (s *Service) ceilingFor(` — однострочная делегация к нему; `spawn.go` потолок только передаёт); строка 145 закрыта D39.122.
|
||||
6. **Baseline и списание.** `baseline = committed` книги на старте попытки (`platform/internal/runs/spawn.go`, греп `baseline, bookCap := m.committed`); списание попытки = разница счётчика с baseline; счётчик ниже baseline = подменённая БД, сеттлится в ноль с WARN — сами цифры в лог не идут (D39.84; `platform/internal/runs/reconcile.go`, греп `the book's meter reads below`).
|
||||
7. ⚠ **Терминальный вердикт ОПЕРАТОРА — путь холда мимо settle (добавлено 22.08, P8-FIX/D39.154).** Прогон, чей движок не ответит уже никогда, закрывает человек: `tmplatformctl run abandon --run <id> --reason <текст> [--release-hold]`. Команда ОТКАЗЫВАЕТ, если попытка ещё называет юнит или несёт базовую линию траты — над живым движком прогон не закрывают. Холд возвращается ЦЕЛИКОМ в обоих случаях (к abandon допускается только попытка, не дошедшая до движка, значит она ничего не потратила), а флаг решает лишь КОГДА — сразу или на ближайшем свипе. ⚠ **С пака P11 (`PD-385`) ВЕТВЕЙ ДВЕ, и абзац выше — только ЖИВАЯ** (испр. ревизией 02.09; образец — секция «Строка PHASE = settling» в `platform/deploy/README.md`). У прогона, чья попытка уже КОНЧИЛАСЬ, команда идёт второй ветвью: отказ «юнит/базовая линия» к ней НЕ применяется, к abandon допускается попытка, которая ДОШЛА до движка и ПОТРАТИЛА, холд всё равно возвращается ЦЕЛИКОМ по каждой осиротевшей попытке, а флаг `--release-hold` там ИГНОРИРУЕТСЯ; единственная защита — порог `abandonAfter` неудач расчёта (отказ `ErrSettlementNotStuck`). ⚠ **Ветвление идёт по `runs.finished_at`, а НЕ по наличию осиротевшей попытки** — ратифицированная оговорка `PD-418`. ⚠ **И вторая ветвь НЕ лечит живую:** прогон с намертво заблокированной расплатой уходит в ЖИВУЮ ветвь и закрыт отказом по юниту до сих пор — `PD-424` стоит `open` именно на этой половине.
|
||||
|
||||
|
|
@ -86,8 +97,9 @@
|
|||
ни на юнит, и воспроизвёлся дважды. Прогон умирает за 10–15 секунд, списание не двигается.
|
||||
|
||||
**ПОЧЕМУ — цепь, проверенная тремя чтениями независимо.** Запас нового прогона на НОВЫЕ траты равен
|
||||
`chaptersLeft × ставка` (`platform/internal/runs/spawn.go`, греп `func (m meter) bookCap`; ставка
|
||||
`DefaultPerChapter = 30_000` микро-USD). Резерв ОДНОГО редакторского вызова — worst case по `maxTokens`
|
||||
⚠ **испр. 06.09** — не `chaptersLeft × ставка` (ставки нет), а **холд от проекции движка**:
|
||||
`k × ожидаемое(заказ) + шаг`, где шаг — крупнейшая НЕДЕЛИМАЯ резервация
|
||||
(`platform/internal/pricing/pricing.go`, греп `func (m Model) Hold(`). Резерв ОДНОГО редакторского вызова — worst case по `maxTokens`
|
||||
(`backend/internal/pipeline/stagerun.go`, греп `func (r *Runner) callEstimateUSD`), замеренный отказ в
|
||||
ряду `PD-440` — `denied estimate=$0.069828`. Отказ резервации трактуется как **авария всей книги**:
|
||||
`runWave` зовёт `cancel()` производного контекста на ПЕРВОЙ ошибке любого рода, и уже допущенные
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
| дверь | откуда следует |
|
||||
|---|---|
|
||||
| `resnapshot --dry-run` | строка **124(в)**, глагол поименован там дословно. ⚠ Родня строки **231**: та просит ту же способность («оценить, ничего не покупая») со стороны ШВА, и решаться они, скорее всего, одним глаголом |
|
||||
| ⚠ движковая оценка $/глава — **ПОСТРОЕНА и опубликована 05.09** (`expected_usd` · `book_once_usd` · `step_max_usd` в `manifest --json`/`status --json`, лендинг `81a89e9`, акт D39.206); непостроена ПЛАТФОРМЕННАЯ половина — числа никем не читаются (`git grep -lE "StepMax|step_max" -- platform/` пусто) | строка **166**. Приоритет поднят: D39.165 §1 ратифицировал цену ОТ ОБЪЁМА исходника, и оценка перестала быть удобством. ⚠ Числа цены главы и их пере-пин под цены с 16.08 — только `15-money-path.md` §3 п.3, здесь копии нет |
|
||||
| ✅ **И ПРОЧИТАНА платформой с 06.09** — механика ОДНИМ носителем: `15-money-path.md` §3 (акт `D39.208`). Прежняя редакция: ⚠ движковая оценка $/глава — **ПОСТРОЕНА и опубликована 05.09** (`expected_usd` · `book_once_usd` · `step_max_usd` в `manifest --json`/`status --json`, лендинг `81a89e9`, акт D39.206); непостроена ПЛАТФОРМЕННАЯ половина — числа никем не читаются (`git grep -lE "StepMax|step_max" -- platform/` пусто) | строка **166**. Приоритет поднят: D39.165 §1 ратифицировал цену ОТ ОБЪЁМА исходника, и оценка перестала быть удобством. ⚠ Числа цены главы и их пере-пин под цены с 16.08 — только `15-money-path.md` §3 п.3, здесь копии нет |
|
||||
| `tmctl init` (форма В) | строка **170**; приоритет ПАДАЕТ после двери 199(а) — та забирает её главный довод |
|
||||
|
||||
⚠ **ПОСТРОЕНЫ по этому закону и из таблицы сняты** (форма читается по ним как по образцам): приём правок банка движковым глаголом `tmctl bank-apply` — 27.08, D39.158, развилка владения `book.yaml` (строка 199а) растворена · `--keys-file` — движок 27.08 (D39.158), передача с платформы 28.08 (P9/D39.162), строка **211** ЗАКРЫТА и блокером живого прогона больше не является (⚠ ⚠ испр. 05.09: строка **202** ЗАКРЫТА D39.200 — прогон отработал; её остаток о непокрытых поверхностях живёт строкой **255**) · путь артефакта в ответе движка — конверт `StatusArtifacts` в `status --json`/`manifest --json` (`project_db` · `bank_export` · `mined_delta` · `mined_rejects` · `book_files`, D39.162/D39.175), строка **213** сужена до одной строки ДЕВ-супервизора · consent-флаги `--resnapshot`/`--accept-rebill` от платформы — 28.08, D39.166, продаются членом `RunRequest.re_pass` (канон контракта 0.7.0+); гейт «строка 192» СНЯТ владельцем (D39.165).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
# ПАК «ФОРМАТЫ И ЧЕСТНЫЙ СЛОВАРЬ» — движковая сессия
|
||||
|
||||
> ⛔⛔ **ОТРАБОТАН, ПРИНЯТ С ДОФИКСОМ И ЗАЛЕНДЕН 06.09 — АКТ `D39.210`, коммит `2f65d1d`. ЗАДАНИЕМ БОЛЬШЕ
|
||||
> НЕ ЯВЛЯЕТСЯ.** Исполнила сессия `textmachine-main-fe`. Файл уведён в архив, чтобы холодная сессия не
|
||||
> взяла заказом то, что уже построено.
|
||||
> ⚠ **Что этот пак НЕ построил и НЕ должен был** (пак 2, отдельным промтом, ПОСЛЕ файлов владельца):
|
||||
> лексиконы не-CJK, корпус и замер оракулом, индуктор скелетов, структурные валидаторы.
|
||||
> ⚠ **Чего в нём НЕТ по цене, и это живёт строкой бэклога 302:** резка xhtml по якорям `nav` — книга,
|
||||
> пакующая несколько глав в один документ с якорями, даёт одну грубую главу; на книге владельца это
|
||||
> замерено ценой 2187 рун печатного оглавления внутри главы.
|
||||
> ⚠ **Открытая половина ревью-вопроса проекта — строка 303:** безъюнитная грамматика («Kapitel 12»)
|
||||
> снимается чанкером, но НЕ режет книгу.
|
||||
> **ВЫДАН 05.09 сессии `textmachine-main-fe`.** Три круга совместного ревью (оркестратор + Fable 5);
|
||||
> состав пака и словарь — итог восьми кругов сходимости, тела разборов в `D39.202`/`D39.207`.
|
||||
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
| ПТ-22 | ⚠ **испр. 05.09: жанровый словарь ОТМЕНЁН как класс (`D39.47`, 26.07), а ПОЛЕ `genre` выкинуто из продукта вайр-батчем и отставка прибита тестом (`D39.195` п.2, греп `genre_retired_test.go`) — два РАЗНЫХ решения, прежняя редакция их смешивала и ссылалась на `D39.104` п.2, которая про единый закон-блок, строка 184 ЗАКРЫТА лендингом вайр-батча — предстоящей работы здесь нет.** Настройки от человека: 18+, жанр и т.п. (V0-п.22) | 🔶 | `book.yaml` (labels) данными; UI = Ф3. ⚠ ЖАНР из продукта ВЫКИНУТ словом владельца 16.08 (D39.138 п.2в, строка 184: ни на чём не ветвится, влияние не измерено; снимается из промптов/брифа/контракта/формы) |
|
||||
| ПТ-23 | Веб-ПЕРВЫЙ фронт; SEO/ранжирование Google — куки, страницы-сироты, актуальные алгоритмы (V5-п.2); продукт = **SaaS**, не локальное приложение (решение владельца 02.08, D39.81 — натив-прицел V0-п.26 СНЯТ этим решением, вернуть только новым словом владельца); SEO только под Google, Яндекс снят; рынок глобальный; десктоп = PWA первым шагом, Tauri по явным триггерам (решения владельца 02.08, D39.84) | ⭕ | строка 71 (Ф3-бриф); SEO вошёл в стек-дизайн ДО выбора (D39.84, требование исполнено): двухконтурный деплой — SPA `app.<домен>` целиком под noindex + статический публичный контур ≤3 кликов от главной (лечение страниц-сирот), `frontend/docs/STACK_DECISIONS.md` §4; SSR-вопрос закрыт этим решением |
|
||||
| ПТ-24 | Цикл согласования глоссария с профпереводчиком/редактором (V0-п.23) | 🔶 | подписной цикл терминолога в бэкенде есть; UI = Ф3. Модель цикла записана словом владельца 16.08 (D39.144 + строка 192): черновой перевод и черновой банк появляются вместе → ОДИН ОК всему банку (не пер-термная масс-подпись) → доработка → пост-ридинговые точечные правки с пере-генерацией по вхождениям (механика — после полигона) |
|
||||
| ПТ-33 | Интерфейс НЕ раскрывает конвейер: ни моделей, ни стадий, ни внутренней терминологии (решение владельца 02.08; граница пере-читана 15.08 — охранять АЛГОРИТМЫ, не минорную механику, D39.136 п.4а). ⚠ «Лестница» фаз «загрузка → … → финал → готово» — устная фраза владельца, НЕ норма продукта (его слово 16.08, D39.138: `finalizing` сворачивается) | 🔶 | инвариант D39.81; словарь статусов принят (D39.100/К-1); карта «вердикт → фраза» — приложение А компаньона (В-3), после D39.138 пишется по словарю машинных кодов Б-1. Машинный ГЕЙТ на утечку волн draft/edit — фронту при разморозке (пинг 16.08; сама утечка снята батчем 0.3.0, D39.142/143). Полоса прогресса — ОДНА монотонная доля на всю работу прогона через обе волны, считает СЕРВЕР, плюс подпись «что делается сейчас» (`Progress.stage`, ВТОРОЕ и последнее исключение границы — D39.163, словарь ОТКРЫТ, клиент рисует незнакомое нейтрально). ⚠ **Остаток, который НЕ закрылся и назван честно:** платформа продаёт ГЛАВЫ, а движку передаёт только денежный потолок — единица продажи не равна единице остановки (`PD-410`, архитектурный стоп; направление лечения — D39.165 §1). И полоса ПЕРЕ-ПРОХОДА гранулярности не имеет вовсе: движок анонсирует работу один раз за жизнь книги, поэтому она объявлена «одной неделимой единицей работы» (D39.166), а не выведена |
|
||||
| ПТ-33 | ⚠ **КВАЛИФИКАТОР 06.09:** «движку едет только денежный потолок» верно для заказа ВСЕЙ КНИГИ (объёмный флаг тогда не добавляется) и НЕВЕРНО для частичного — там в argv едет `--max-units`. И платформа продаёт уже не только главами: заказ — `chapters` ЛИБО `characters`. Требование скрытия конвейера этим не нарушено, но формулировка ниже сужена. Интерфейс НЕ раскрывает конвейер: ни моделей, ни стадий, ни внутренней терминологии (решение владельца 02.08; граница пере-читана 15.08 — охранять АЛГОРИТМЫ, не минорную механику, D39.136 п.4а). ⚠ «Лестница» фаз «загрузка → … → финал → готово» — устная фраза владельца, НЕ норма продукта (его слово 16.08, D39.138: `finalizing` сворачивается) | 🔶 | инвариант D39.81; словарь статусов принят (D39.100/К-1); карта «вердикт → фраза» — приложение А компаньона (В-3), после D39.138 пишется по словарю машинных кодов Б-1. Машинный ГЕЙТ на утечку волн draft/edit — фронту при разморозке (пинг 16.08; сама утечка снята батчем 0.3.0, D39.142/143). Полоса прогресса — ОДНА монотонная доля на всю работу прогона через обе волны, считает СЕРВЕР, плюс подпись «что делается сейчас» (`Progress.stage`, ВТОРОЕ и последнее исключение границы — D39.163, словарь ОТКРЫТ, клиент рисует незнакомое нейтрально). ⚠ **Остаток, который НЕ закрылся и назван честно:** платформа продаёт ГЛАВЫ, а движку передаёт только денежный потолок — единица продажи не равна единице остановки (`PD-410`, архитектурный стоп; направление лечения — D39.165 §1). И полоса ПЕРЕ-ПРОХОДА гранулярности не имеет вовсе: движок анонсирует работу один раз за жизнь книги, поэтому она объявлена «одной неделимой единицей работы» (D39.166), а не выведена |
|
||||
| ПТ-34 | Пользовательский перевод НИКОГДА не попадает на публично индексируемый URL — ни витрин примеров, ни «поделиться главой», ни публичных ссылок чтения (жёсткий продуктовый запрет, решение владельца 02.08; авторское право на исходники + политика Google по масштабируемому машин-контенту действуют на весь домен) | ⭕ | инвариант D39.84; носители: двухконтурный деплой `frontend/docs/STACK_DECISIONS.md` §4 (SPA под `X-Robots-Tag: noindex` на каждом HTML-ответе; публичный контур без контента переводов) + контракт 14 (`architecture/14-api-contract/`, D39.99) + платформенный бэклог П-1; правило доступа к экспорт-ссылке (срок/подпись) — батч 183 (research/28 Б-4) |
|
||||
| ПТ-35 | Лимиты «как в Claude Code» (решение владельца 04.08, D39.100): страница лимитов/использования в настройках (статус, НЕ суммы) ⚠ **испр. 05.09: «запрет денег на экране» ОТОЗВАН владельцем — D39.196, эррата 05.09-а; и ВТОРОЕ уточнение `D39.203`: движку РАЗРЕШЕНО говорить платформе «сколько добавить», потому что наружу идёт ДО-ВЫЗОВНАЯ ОЦЕНКА (резервация ПЕРЕД звонком), а запрет ниже написан про СТОИМОСТЬ — что списано по факту. Сеттлмент не выходит ни одной дверью**; баланс аккаунта, потолок заказа и холд выходят наружу ДЕНЬГАМИ, поля приезжают паком формы заказа. Запрещённым осталось другое и оно не отменялось: цены моделей, стоимость стадий и вызовов; стоп по потолку = статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны» | 🔶 | контракт К-8 закрыт, спека несёт `paused`/`paused_reason` (0.2.0+, D39.115); ручки /v0 — платформа-раннер P4 (D39.123); статус `paused` по потолку ЧЕСТЕН с P6 (D39.132: exit 4/событие → `paused`, не `failed`; дневной потолок оператора различим и из платформенного шаблона убран — решение владельца 15.08); остаток: страница лимитов и оповещение на фронте (после РАЗМОРОЗКИ зоны: лендинг P7 её НЕ размораживает — D39.147) · ⚠ фразы (В-3 · К-6 · Приложение А) ДЕЛЕГИРОВАНЫ проекту словом владельца 30.08 (D39.176 п.4): подписи владельца НЕ ждём, требование ИНЖЕНЕРНОЕ — фраза не литерал ни в Go, ни в TSX, а ДАННЫЕ по машинному коду причины и локали; вместе со словарём закрывается `PD-246`, гейт — строка бэклога 204 · К-13 ЗАКРЫТ (D39.132 п.2а: `null` на проводе) |
|
||||
| ПТ-36 | Интерфейс МУЛЬТИЯЗЫЧНЫЙ, русский не хардкодится в разметке (слово владельца 10.08; язык интерфейса — отдельная ось от языков перевода) | 🔶 | ПОСТРОЕНО S3.7 (принят D39.129): каталог `frontend/src/i18n/ru.ts` (⚠ число ключей здесь НЕ держим — оно растёт каждым касанием зоны; счёт снимается командой `grep -cE "^\s+'[a-z][a-zA-Z0-9.]*':" frontend/src/i18n/ru.ts`) · механизм `@internationalized/string` · ESLint-гейт «текст местом» · стор языка с персистом · `<html lang>` следует выбору; содержимое — только ru; продуктовый список языков — 🔒 владелец (В-9 зоны фронта) |
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
> Отдельные утверждения отсюда становятся ратифицированными только собственной D-нотой; статус файла
|
||||
> в целом — материал для решения.
|
||||
>
|
||||
> ⛔ **ИСПР. 06.09 ПО КОНСИЛИУМУ (тело НЕ переписано, `D23.3`): рекомендация «не покупать судейских панелей» (строка 345) обоснована НЕ ТЕМ.** Там сказано «парный судья назвал победителя в 29 голосах из 30 на чистом шуме», но вывод замера нёс не этот счёт: блокирующий порог по нулям НЕ сработал, и сам отчёт замера это печатает — «непрохождение блокирующего порога исправность прибора не доказывает», а операционный исход был «РАЗЛИЧИЕ НЕ ПРЕДЪЯВЛЕНО», то есть прибор дисквалифицирован НЕ БЫЛ. Вывод нёс **пре-регистрированный критерий сравнения с боевыми** (точный тест Фишера) плюс **потолок счётчика дефектов**. ⇒ рекомендация по существу стоит, а её основание в строке 345 — пересказ, который сильнее оригинала. ⚠ Соседний носитель `docs/PROGRESS.md` то же число ПРОВЕРЕН и НЕ ТРОГАЕТСЯ: он сообщает его рядом с первичными исходами и «различие НЕ ПРЕДЪЯВЛЕНО», то есть не делает основанием.
|
||||
>
|
||||
> ⚠ **ФОРМУЛИРОВКА, БЕЗ КОТОРОЙ ЗАМЕРЫ ТОПОЛОГИИ ЧИТАЮТСЯ НЕВЕРНО (§1.4):** **судья как СТАДИЯ
|
||||
> ДВИЖКА не построен и не вызывался — 0 вызовов.** Судьи в проекте были и есть, но только в ПОЛИГОНЕ,
|
||||
> на стенде. Всё, что здесь и в соседних отчётах написано «→ судья», описывает эксперимент, а не
|
||||
|
|
|
|||
739
docs/research/33-backend-debt.md
Normal file
739
docs/research/33-backend-debt.md
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
# research/33 — Техдолг бэкенда: замер цены при изменении и что обязан перепроектировать перекрой глав
|
||||
|
||||
> **⟶ СТАТУС: ФАКТУРА, НЕ РАТИФИЦИРОВАНА.** Написан контролёром 06.09 по заказу владельца («итог замера
|
||||
> техдолга — ресёрч-документом, чтобы по нему принимать решения»). Отдельные утверждения отсюда становятся
|
||||
> ратифицированными только собственной D-нотой; статус файла в целом — материал для решения. Строки бэклога
|
||||
> по нему заводит оркестратор; сам файл ничего в дереве не меняет и не заказывает.
|
||||
>
|
||||
> **ЧЕМ СНЯТО.** Ветка `main`, HEAD `e69902a` на момент письма. Замер шёл воркфлоу из **38 агентов**
|
||||
> (6 срезов-проб · 31 опровергатель · 1 сводный вердикт; 4.32 млн токенов, 1866 вызовов инструментов) на
|
||||
> деревьях `d8f723f`/`6457d77`/`8d61663`/`878cac5` — **бэкенд на всех четырёх побайтово равен HEAD**
|
||||
> (`git diff --stat <c> HEAD -- backend/` пуст, сверено). ⚠ Один опровергатель назвал деревом `535f60a`
|
||||
> (05.09), где `backend/` отличается на 32 файла (пак структуры глав `2f65d1d`); его адреса перепроверены по
|
||||
> HEAD и сходятся. Каждую находку отдельный агент пытался **опровергнуть** с установкой «по умолчанию это не
|
||||
> тормозит»; в документ вошли **11 переживших**, **20 снятых** — с доводом (§7), 8 косметических не оспаривались
|
||||
> (§7.1). Поверх воркфлоу — моя сверка: адресные грепы по HEAD и **четыре агента-воспроизводителя**, гонявшие
|
||||
> мутации на копиях дерева с полной батареей (дерево не трогалось).
|
||||
>
|
||||
> **ЧТО МЕРИЛИ — цену ПРИ ИЗМЕНЕНИИ, не красоту кода.** Шесть срезов: вторые носители одного знания · цена
|
||||
> одной правки (сцепление со снапшотом / хешем запроса / ключом манифеста, то есть с перепокупкой) · общность
|
||||
> движка («заработает ли пара, которой в репо нет, без правки Go») · швы и ответственность · форма тестов как
|
||||
> долг · инертность по git-истории.
|
||||
>
|
||||
> **КАК ЧИТАТЬ МЕТКИ.** ✔ — сверено мной по HEAD (греп/чтение, адрес `file:line` живой) · ⟲ — воспроизведено
|
||||
> исполнением на копии дерева моим агентом-верификатором 06.09 (мутация + полная батарея
|
||||
> `go test ./internal/... -count=1`) · **(агент)** — взято из отчёта агента замера, мной не сверено · «вывод» —
|
||||
> установлено рассуждением, не замером. **Комментарий в коде поведения не доказывает**: источником смысла
|
||||
> здесь служат тест или прогон, комментарии цитируются как улика того, что проект ЗНАЕТ, а не что код ДЕЛАЕТ.
|
||||
>
|
||||
> **ПОПУЛЯЦИИ ЧИСЕЛ** (одно «число тестов» дало у трёх наблюдателей 1157/1246/1252 — три разных популяции):
|
||||
> 340 `.go` = 138 прод + 202 тест; 46 212 строк прод-кода / 54 489 тестов (1.18); `^func Test` — **1157 в
|
||||
> `internal/`**, **1246 по всему `backend/`** (с `cmd/`), **1252** с Benchmark/Fuzz ✔. Батарея на HEAD
|
||||
> `e69902a`: 18 `ok` + 2 без тестов, EXIT 0 ✔. `internal/pipeline` — 17 236 строк / 41 файл = 37 % прод-кода ✔.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ответ владельцу одной оценкой
|
||||
|
||||
**Бэкенд НЕ техдолговый в том смысле, о котором спрашивает владелец: он не инертный, он недосторожённый.**
|
||||
Разработку код не тормозит — тормозит одна НОРМА, наведённая денежной моделью (копить wire-правки одним
|
||||
касанием, §2.А.1). Из одиннадцати долгов, переживших проверку цены, вердикт «переписать» не получил **ни
|
||||
один**; все одиннадцать — «развязать точечно». Это не мягкость опровергателей: каждый из них доказал
|
||||
исполнением, что «переписать» здесь дороже развязки, и почти всегда — ровно на ту перепокупку оплаченных
|
||||
книг, которой владелец просил избежать.
|
||||
|
||||
Долг есть, он реальный, и он **одного класса: гарантии держатся прозой, фикстурами и дисциплиной, а не
|
||||
гейтами**. Шесть из одиннадцати находок (Д1, Д2, Д3, Д9, Д10, Д11) — «шов не сторожится ничем», и три из них
|
||||
доказаны до артефакта: мутант, убирающий восстановление заголовка из манифеста, оставляет батарею зелёной, а готовая книга
|
||||
теряет ВСЕ названия глав (Д10/Д11); мутант, сдвигающий заголовок на последний чанк, тоже зелёный; на
|
||||
нулевой главе `第零章` ингест режет, чанкер заголовок не снимает, и сырой CJK-маркер уезжает модели (Д3).
|
||||
Это риск КАЧЕСТВА, не риск скорости.
|
||||
|
||||
**Второй противовес: долг в основном ЗАРЕГИСТРИРОВАН, а не спрятан.** Бэклог — 230 строк ✔; у шести из
|
||||
одиннадцати находок есть строка или D-нота (Д2, Д4, Д5, Д6, Д8, Д9); у пяти носителя нет — Д1, Д3, Д7, Д10,
|
||||
Д11: четыре из них — непокрытый шов, Д7 — недостроенный канал данных; ни одна не архитектурная.
|
||||
|
||||
**Здоровые части — поимённо, с уликой** (не вежливость, замерено):
|
||||
|
||||
- **Граф импортов — DAG без циклов** (`go list -deps` проходит ✔); `internal/pipeline` — ЛИСТ: его импортируют
|
||||
только 6 файлов `cmd/tmctl` и `internal/archguard` ✔. Практически: 17-тысячестрочный пакет — чердак, а не
|
||||
фундамент; перекрой внутри него не ломает ничего снаружи на этапе компиляции.
|
||||
- **Архитектурные инварианты исполняет машина, не дока:** `internal/archguard` — три go/analysis-анализатора
|
||||
(`StageSeam`/`RequestSeam`/`EgressSeam`, `archguard.go:79-81` ✔) под `go vet -vettool` из `make battery`;
|
||||
в шапке честно перечислено, что НЕ ловится, и на каждый класс лежит фикстура без ожидания (агент).
|
||||
- **Провод пинится побайтово:** `golden_test.go:23-29` ✔ — снапшот, каждый `request_hash`, тела запросов и
|
||||
итоговые тексты для свежего прогона И резюма; «a changed request_hash = a missed checkpoint =
|
||||
--resnapshot = the whole book re-billed (D15), which no code-health win can pay for».
|
||||
- **Денежный контур сделан аккуратно:** `repin.go` 329 · `rebill.go` 566 · `snapshotdiff.go` 184 строки ✔,
|
||||
16 зелёных тестов (агент); предикат ре-пина консервативен по построению — незнакомое поле считается
|
||||
различием (`repin.go:61-91` ✔); гейт согласия отказывает ДО единого вызова; стоп называет уехавшую ось.
|
||||
- **Собственный мутационный каталог** `cmd/tmmutate/mutations.json` — 211 мутаций на 43 файла ✔; проект уже
|
||||
практикует ту дисциплину, которой его мерили. Дыры ниже — незакрытые углы ВНУТРИ действующей дисциплины.
|
||||
- **Грамматика главы — данные:** `<root>/<src>/structure.txt` (`lang/structure.go:142` ✔), и дежурный
|
||||
ревью-вопрос задан ТЕСТОМ, а не намерением: `TestALanguageAbsentFromTheRepositoryCutsFromDataAlone`
|
||||
(`structure_newlang_test.go:16` ✔; корейский, которого в репо нет, режется одним файлом — ⟲ `markers=["제"]`).
|
||||
- **Образец правильного разреза плоскостей уже стоит:** `lang/bankdata.go:14-35` ✔ — вторая эмбед-плоскость с
|
||||
собственным `bankDataAlgoVersion`, вне волнового снапшота, под стражем `TestBankPlaneIsOutsideTheWaveFold`
|
||||
(`bankdata_test.go:13` ✔). Лечение Д4 — повторить этот разрез, а не изобретать.
|
||||
- **Миграции** — один append-only список, версия = длина списка (агент); идемпотентные шаги.
|
||||
- **По оси общности переписывать нечего:** в прод-Go 0 захардкоженных слагов моделей ✔, 0 маркеров
|
||||
TODO/FIXME/HACK ✔ (единственный хит — слово «hack» в комментарии `quality.go:50`), ветвление по языку в
|
||||
боевом коде одно и документированное (`ingest.go:493`, GB18030-проба — агент).
|
||||
- **История не показывает буксования:** rework-rate 14.1 % с учётом переименований, 7.4 % коммитов-исправлений,
|
||||
недельная выработка последних двух недель на уровне первых (агент, срез инертности). Раскол год-пакета
|
||||
(`e33e3be`, 44 файла) уже проведён и вышел с зелёным сьютом — механизм крупных перекроев отработан.
|
||||
|
||||
**Единственный вектор, за которым стоит следить как за здоровьем:** `Runner` — 16 полей на 25.07 (`e33e3be`)
|
||||
→ 25 на 31.07 (`8b1d8d1`) → **39 на 06.09** ✔ (агент замера насчитал 18 на 25.07 — другой коммит того дня;
|
||||
направление то же), 178 методов в 29 файлах ✔, файл трогали 49 коммитов ✔. Но замеренная цена этого роста
|
||||
сегодня ровно одна и узкая — непиненный порядок предвычисления (Д9), и лечится она часами, не паком.
|
||||
|
||||
---
|
||||
|
||||
## 2. Два разных списка
|
||||
|
||||
### 2.А. Что тормозит РАЗРАБОТКУ сегодня (счёт выставляется сейчас)
|
||||
|
||||
1. **Норма «копить wire-правки и вносить одним касанием» (строка 182 ✔) — единственная измеренная
|
||||
НАВЕДЁННАЯ инертность.** Снаружи текущего батча стоят и названы вслух: 131 (кап инъекции), 105(а)
|
||||
(вердикт-половина классификатора), 14 (ё-фолд), 119+124+115 (одним дизайн-паком), 160, 142 — часть из
|
||||
них чистая $0-гейтовая работа. Ждут не из-за риска, а потому что раздельный лендинг стоит денег. Корень
|
||||
механический: `classifySnapshotMove` знает ровно одно re-pinnable поле — `const memoryVersionField`
|
||||
(`repin.go:54` ✔), всё остальное = `moveOther` = не пере-пиньится ничего (Д5).
|
||||
2. **Лендинг данных заблокирован порядком.** `docs/PROGRESS.md:31` ✔ — стоячий запрет класть правку
|
||||
`internal/lang/data/*.txt`, пока платная книга живая. И прямо в дереве: `lang/structure.go:108-110` ✔ —
|
||||
правка `cjk-section.txt` «would move EmbeddedVersion and re-cut every book alive **for a refactor that
|
||||
changes no behaviour**»; `DefaultCJKStructure` пришлось ВЫВОДИТЬ из файла, а не переписать, именно чтобы
|
||||
это обойти. Поведенчески-нейтральный рефактор в `internal/lang` физически дороже, чем должен быть (Д4).
|
||||
3. **Порядок предвычисления не пиненный, и один счёт уже не оплачен.** `bookrun.go` — 0 записей из 211 в
|
||||
собственном каталоге мутаций (то же у `seeding.go`, `snapshot.go`, `bankexport.go`, `repin.go`) при густом
|
||||
покрытии соседей (`bankdecisions.go` 20–21, `volume.go` 11, `waverun.go` 8–11) ✔. Живое следствие — строки
|
||||
224/253 ✔: `mining.go:269` `exportBank("bank-mining/signature-stop")` стоит СНАРУЖИ ветки `if !stopping`
|
||||
(`:234`), а наполняющие банк `writeAutoBank` (`:241`) и `seedGlossary` (`:244`) — ВНУТРИ ✔; экран подписи
|
||||
читает пустой артефакт (`total 0, signed 0` при 82 неразобранных поверхностях — найдено первым живым
|
||||
прогоном 04.09, строка 253 ✔). `tmmutate` не в батарее: `Makefile:58` `battery: build vet fmt lint test`,
|
||||
слова `mutate` в Makefile нет ✔ (Д9).
|
||||
4. **Шов заголовка главы не утверждается нигде.** Три мутанта переживают всю батарею; причина —
|
||||
вырожденные фикстуры двух тестов, чьи имена обещают ровно эту гарантию (Д10/Д11).
|
||||
|
||||
**НЕ тормозит — проверено историей, а не ощущением:** «правка в четырёх местах при каждой смене стека»
|
||||
(`ThinksOnWire`, Д2) — стек моделей менялся, смены ушли в `configs/models.yaml`
|
||||
(`capabilities.reasoning.control`), а блок армов `applyToBody` побайтово одинаков во всех 6 коммитах файла с
|
||||
04.07 ⟲. Закрытые словари классификатора (Д1) — на zh-ru-работе цена при изменении ноль. Нулевой канал langpack (Д7) — ни разу не оплачен: 0 файлов
|
||||
`manifest.txt` в `configs/` ✔, каталог пары один (`configs/langpacks` = `ru`, `zh`, `zh-ru` ✔).
|
||||
|
||||
### 2.Б. Что удорожает БУДУЩИЙ перекрой структуры глав (сегодня не платится)
|
||||
|
||||
1. **`embedded_version` — один хеш на 8 файлов, три потребителя, три радиуса взрыва** (Д4): `snapshot.go:443`
|
||||
(провод/вердикт волн) + `manifest.go:264` и `:366` (CUT-тег и ключ манифеста) ✔. Правка `target-ru.txt`
|
||||
($0-словарь вердикта) двигает CUT-тег, хотя `TargetChecksFor` не имеет ни одного call-site в
|
||||
`internal/chunk` и `internal/text` ✔ — то есть физически не может повлиять на нарезку.
|
||||
2. **`structure_version` вне снапшота + `projectRebill` слеп к контентной/позиционной оси** (Д6, «дыра Р6»,
|
||||
`rebill.go:33-36` ✔, строка 160 ✔): ревизия структуры через данные перекупила бы хвост МОЛЧА. Порядок
|
||||
величины назван проектом — ~$9–11 на 500 глав за вставку в середину (строка 185 ✔).
|
||||
3. **Ординал главы несёт три семантики на одном int** (Д8): позиция — PK `chunk_status` (`migrate.go:126` ✔);
|
||||
время — `spoilerBlocked` (`memory.go:678-686` ✔); граница — `chapterNo++` (`chunker.go:140` ✔). Радиус
|
||||
замерен: **183 не-тестовых площадки** — membank 70 · store 47 · pipeline 43 · miner 7 · seed 6 ·
|
||||
terminology 5 · chunk 3 · runevents 1 · obs 1 ✔. **64 % (117/183) лежит вне `internal/pipeline`** —
|
||||
переписывание шины его не покупает.
|
||||
4. **`unit_resolutions` платформы ключеваны позицией** (строка 298 ✔) — любой перекрой сносит их целиком;
|
||||
сегодня цена нулевая (решения есть только у стенд-книги), после интейка — нет.
|
||||
5. **Правило заголовка разъехалось по трём предикатам** (длина 60 рун · осмысленность номера `v<=0` ·
|
||||
обязательность юнита), и ни один не пришпилен ни с одной стороны (Д3). Живой дефект уже сегодня:
|
||||
`第0章`/`第零章`/`第〇章` — ингест главу режет, чанкер заголовок не снимает ⟲, плюс номера главы и заголовка
|
||||
расходятся (`第一章` становится `Chapter 2`, но рендерится «Глава 1» ⟲).
|
||||
|
||||
---
|
||||
|
||||
## 3. Поимённо: одиннадцать переживших долгов
|
||||
|
||||
Нумерация — по итогу воркфлоу. Для каждого: предмет · улика · цена при изменении · вердикт · **двигает ли
|
||||
лекарство снапшот / хеш запроса / ключ манифеста / версию встроенных данных** (= означает ли перепокупку уже
|
||||
оплаченных книг) · носитель. Д10 и Д11 — один шов, найденный двумя срезами (форма тестов и швы), разбираются
|
||||
вместе.
|
||||
|
||||
### Д1. Закрытые словари классификатора живут в Go и словами в промте пары; для НОВОЙ пары гейта нет
|
||||
|
||||
- **Улика ✔:** `internal/terminology/classify.go:18` `Types = {name, place, title, term}`, `:50` `Genders`,
|
||||
`:26` `CandidateTypes` (+`nickname`); третья копия — `internal/pipeline/banknote.go:80` `bankTypeOK`; те же
|
||||
слова прозой в `prompts/zh-ru/classifier.md`. Леджер меток промтов по построению смотрит только zh-ru:
|
||||
`promptlabel_test.go:53` `const pair = "zh-ru"`. Off-vocabulary строки ответа — только
|
||||
`Log.WarnContext` (`terminologist.go:570`), отказа нет. `bankTypeOK` не упомянут ни одним тестом (0 файлов) ✔.
|
||||
- **Замер ⟲:** переименование классов в zh-ru → красен ровно один тест `TestPromptLabelsPinTheirBytes`
|
||||
(сообщение — про сдвинувшийся sha, ни слова про словарь), документированный `TM_UPDATE_PROMPT_LABELS=1`
|
||||
перезаписывает одну строку `testdata/prompt-labels.json` → батарея зелёная. Новая пара
|
||||
`prompts/ja-ru/classifier.md` с локализованными словами ответа (`人名|地名|称号|用語`) → батарея зелёная **без
|
||||
единого шага автора** (линт прошёл по 12 файлам, включая новый). Сквозной прогон с фейк-провайдером,
|
||||
отвечающим чужими словами: работа завершается, чекпойнт классификатора оплачен, `Reclassified=0`, реалия банкуется как `place` — ровно черновая эвристика, ради
|
||||
снятия которой фаза построена (агент, не воспроизводил).
|
||||
- **Цена при изменении:** сегодня на zh-ru — 0. Платится один раз, при второй паре, молчаливой потерей
|
||||
качества (тип неверен 12–22 % и `name/place` форсирует транслитерацию — D39.65, агент) плюс мелким счётом
|
||||
(потолок фазы $1/книга).
|
||||
- **Вердикт: развязать точечно.** Линт КАНОНИЧЕСКОЙ формы промта (с снятыми комментариями — наивная версия
|
||||
даёт ложно-зелёное, слова есть в HTML-шапке) в уже существующем `internal/config/prompt_lint_test.go`
|
||||
(89 строк ✔, сегодня линтует `<TAB>`-плейсхолдеры и сам объявляет целью «ja-ru or en-ru pack authored by
|
||||
copying the zh-ru one»); прототип ~54 строки, краснеет на подложенном ja-ru, зелен на репо (агент).
|
||||
Плюс свести `bankTypeOK` к `terminology.CandidateTypes` (одна строка импорта; уже экспортирован).
|
||||
- **Двигает ли:** нет — только тест. Обратная дорога («переписать»: словарь в данные, байты промта) двигает
|
||||
`prompt_sha256`/`RequestHash`/`classifierVersion`, а **D39.194 п.9 и D39.199 п.5 ратифицировали
|
||||
«`classifierVersion` НЕ двигать»** ✔.
|
||||
- **Носитель:** нет (строки 120/258 несут КЛАСС «ни один тест не отвечает на ревью-вопрос», 204 — платформенная
|
||||
форма; агент). D39.70 санкционировала книжные примеры в `classifier.md`, про идентификаторы классов — ни слова.
|
||||
|
||||
### Д2. `Models.ThinksOnWire` пересказывает `Capability.applyToBody`; копий три, каждая пришпилена к своей рукописной таблице
|
||||
|
||||
- **Улика ✔:** `config/models.go:389-407` `ThinksOnWire` — комментарий «Must mirror applyToBody exactly»;
|
||||
`models.go:519-535` `echoMineViolation` — ЧЕТВЁРТАЯ версия той же таблицы, и это жёсткий fail-fast загрузки
|
||||
(агент: `models.go:290` зовёт `bad(...)`); `llm/capability.go:223` — арм `if effort == "" || effort == "off"`;
|
||||
enum-валидатор `models.go:667-671` не пускает чужую строку — новый control нельзя сконфигурировать, не открыв
|
||||
тот же файл. **Проза гниёт, обе штуки в дереве:** `capability.go:78` «effort "" leaves the provider
|
||||
default» — опровергается `:223`; `models.go:404` «no switch reaches the wire» — ложно для `ReasoningNone`
|
||||
с явным уровнем (`capability.go:236-238` пишет `reasoning_effort`).
|
||||
- **Замер ⟲:** сужение арма до `if effort == "off"` → падает только
|
||||
`TestCapabilityWireBody/GLM_extra_body_disable:_UNSET_effort_also_disables…` (`capability_test.go:133`);
|
||||
легальное обновление этого одного ожидания → **полная батарея зелёная при разъехавшемся зеркале**: провод
|
||||
оставляет thinking на дефолте провайдера, `ThinksOnWire` отвечает «подавляет», предупреждение об эхо-зоне
|
||||
врёт, и ни один тест не возражает.
|
||||
- **Цена:** единственный потребитель зеркала — `sourceEchoExposure` → `logger.Warn` (`runner.go:384-385` ✔,
|
||||
«loud, not fatal»); врущее зеркало гасит РАННЕЕ предупреждение, перевод не портит. Частота ⟲: файл трогали 6
|
||||
коммитов, блок `switch c.Reasoning.Control` байт-в-байт одинаков во всех шести с создания 04.07 (`636919c`);
|
||||
смены стека ушли в `configs/models.yaml` (`capabilities.reasoning.control` в строках 230/240/270/285/320).
|
||||
Прецедент был: предикат родился неверным 25.07 и чинился 02.08 — нашли паком, не гейтом (D39.91 п.4(б),
|
||||
агент).
|
||||
- **Вердикт: развязать точечно, ~40 строк, полдня, вне окна перекроя.** Экспортировать
|
||||
`func (c Capability) SuppressesThinking(effort string) bool` рядом с `applyToBody`; `ThinksOnWire` и switch
|
||||
в `echoMineViolation` — в делегацию; 8 строк таблицы `config/capability_test.go` перенести в таблицу `llm`,
|
||||
чтобы ОДИН кейс утверждал и тело провода, и предикат. Цикла нет: `internal/llm` не импортирует `config`.
|
||||
- **Двигает ли:** нет. Снапшот фолдит `json.Marshal(r.Models.ResolveCapability(model))`
|
||||
(`snapshot.go:535` ✔) — добавление МЕТОДА JSON-форму `llm.Capability` не меняет. Переписывание слоя
|
||||
(смена формы) двинуло бы снапшот каждой стадии.
|
||||
- **Носитель:** строка 114 ✔ (протухший комментарий висит с 02.08); зеркало как дизайн не ратифицировано.
|
||||
|
||||
### Д3. Правило заголовка разъехалось по трём предикатам; третий («第0章») ломает пролог; ни один не сторожится
|
||||
|
||||
- **Улика ✔:** `ingest.go:307` `const chapterHeaderMaxRunes = 60`, применён в `:323`; в `chunker.go`
|
||||
`RuneCountInString` — 0 хитов; `chunker.go:316` `if !any || v <= 0 { return 0, false }` —
|
||||
`parseSectionNumeral` отвергает нулевой номер, который регекс ингеста принимает.
|
||||
- **Замер ⟲:** `第零章:序幕 / 第一章 起始 / 第二章 继续` через боевой `IngestEncoded` → 3 главы; `SplitChunks`
|
||||
даёт `Chapter=1 Heading="" Text="第零章:序幕\n…"` — сырой маркер уезжает модели; то же для `第0章`;
|
||||
`parseSectionNumeral("零"/"0"/"〇") = 0,false`. Побочно: **номера расходятся** — `第一章` = `Chapter 2` /
|
||||
«Глава 1», `第二章` = `Chapter 3` / «Глава 2». Мутация А (страж длины В чанкер) → батарея зелёная; мутация Б
|
||||
(снять страж ИЗ ингеста) → зелёная. Живое расхождение на HEAD: строка-заголовок в 85 рун — ингест `false`,
|
||||
чанкер `n=3 ok=true`. Перекрёстного теста «оба видят одно» нет ни в одном из 97 тестов `internal/chunk` ✔⟲.
|
||||
Здоровая половина сужает предмет: классы числительных и content/separator сведены (агент: 0 расхождений на
|
||||
194 528 рунах), и заявленная длинная ось на четырёх боевых книгах стоит ноль (агент: 250 778 строк,
|
||||
guzhenren 2283/2283).
|
||||
- **Цена:** сегодня на оплаченной книге — 0; на веб-новелле с прологом «第零章» — титульный хаос, ради снятия
|
||||
которого построена политика пак-13. Частота правок: `ingest.go` 9 коммитов, `chunker.go` 6, оба сразу 4 ✔ —
|
||||
платится ~раз в месяц и дёшево (последний акт `2f65d1d` — сведение ДАННЫХ, не повторный вывод правила).
|
||||
- **Вердикт: развязать точечно** — ОДИН перекрёстный тест на реальных формах и сведение двух из трёх
|
||||
предикатов (длина, `v<=0`) к ингестовским стражам; ось обязательности юнита не трогать (D39.207 → вход в
|
||||
пак 2, строка 303 ✔). «Переписать» не покупает ничего: research/27 §6 п.2 ✔ — новый детектор = бамп
|
||||
`chunkerVersion` = обе волны на 100 %; там же §6 «что переиспользуется» ✔ оставляет `matchHeaderLine +
|
||||
parseSectionNumeral` живыми в роли готовой грамматики.
|
||||
- **Двигает ли: ⚠ ДА — чанкерная половина.** Идентичность главы не движется (chapter-id считается по
|
||||
ингестированному тексту ДО `stripHeading`, `manifest.go:209`, агент), но снятие заголовка меняет ТЕКСТ чанка,
|
||||
который уходит на провод, а `chunkerVersion` фолдится в снапшот (`snapshot.go:344/428` ✔; собственная дока
|
||||
константы `render.go:55-58` ✔: изменение того, где начинается глава, «is a deliberate re-translation and
|
||||
must not look current»). ⇒ по дисциплине проекта фикс — это бамп `chunkerVersion` → снапшот обеих волн →
|
||||
громкий `--resnapshot`. **Сводный вердикт воркфлоу поставил этот фикс в шаг 0 как $0 — я с этим не согласен**
|
||||
(§5): в шаг 0 идёт только ЗНАНИЕ и заготовка теста, сам фикс — в окно перенарезки, где `chunkerVersion`
|
||||
бампается всё равно. Единственный законный обход — ратифицировать, что бамп при байт-идентичности оплаченных
|
||||
книг не нужен; это решение владельца/оркестратора, не сессии (D39.183: заказанность решает заказ).
|
||||
- **Носитель:** только ось юнита (строка 303, D39.207). Длина и нулевой номер — нигде: `isChapterHeader`
|
||||
не встречается ни в одном живом доке, `matchHeaderLine` — в двух (строки 160/303 и research/27) ✔⟲.
|
||||
|
||||
### Д4. `embedded_version` — один хеш на 8 файлов с тремя радиусами взрыва; правка $0-словаря двигает CUT-тег
|
||||
|
||||
- **Улика ✔:** `lang/embedded.go:22` — явный `//go:embed` на 8 файлов; `:25-27` — бамп `embedAlgoVersion`
|
||||
только при смене набора/порядка. Потребителей три: `snapshot.go:443` (безусловный фолд, D39.64 П0) +
|
||||
`manifest.go:264` (`cutInputs`) + `:366` (ключ манифеста). `TargetChecksFor` — call-site в
|
||||
`bankmaterialize.go:307`, `runner.go:402`, `sanitizer.go:201`; в `internal/chunk`/`internal/text` — 0 ✔.
|
||||
То есть `target-ru.txt` не может повлиять на нарезку, но двигает `cutTag` → ключ манифеста → на платформе
|
||||
`delete from unit_resolutions where book_id` (строка 198 ✔, `PROGRESS.md:31` ✔). Хеш воспроизведён
|
||||
независимо и совпал с голденом `capture.golden:3` (агент). `lang/structure.go:108-110` ✔ — проект уже
|
||||
ЗАПИСАЛ, что поведенчески-нейтральный рефактор здесь стоит перенарезки всех книг.
|
||||
- **Частота ✔:** 8 коммитов в `internal/lang/data/` за жизнь репо, все 24.07–01.08; `target-ru.txt` — 6;
|
||||
с 01.08 — ни одного. Деньгами сегодня дёшево (D39.60: «$0.3–0.5 корпуса»; D39.64 «перекупок НЕТ» — потому
|
||||
что живых оплаченных книг тогда не было). Платится другим — порядком лендинга и робостью рефактора (§2.А.2).
|
||||
- **Гейта нет, есть дисциплина:** `embedded_test.go:12` `TestEmbeddedVersionMutation` ✔ утверждает ОБРАТНОЕ
|
||||
желаемому (любой байт любого файла обязан двигать хеш); гасят цену только D39.62 Q3 ✔ («расщепление хеша
|
||||
пар-данных — НЕ делаем; дисциплина взамен: правки данных копятся в тот же resnapshot-батч») и ручной
|
||||
запрет `PROGRESS.md:31`.
|
||||
- **Вердикт: развязать точечно** — довести до ТРЁХ радиусов (нарезка / провод / вердикт) по образцу
|
||||
`bankdata.go:14-35` ✔. Честная цена: 24 строки + три call-site + переписывание `TestEmbeddedVersionMutation`
|
||||
(обслуживание по D39.183 — ТОЛЬКО если пак это закажет, и объявляется в отчёте).
|
||||
- **Двигает ли: ДА — сам ремонт.** Смена набора/схемы хеширования требует бампа `embedAlgoVersion`
|
||||
(`embedded.go:25-27` ✔) → `EmbeddedVersion` → снапшот + CUT-тег → одна глобальная перенарезка. Ремонт
|
||||
отдельным актом платит ровно ту цену, которую отменяет ⇒ единственный дешёвый маршрут — ВНУТРЬ уже
|
||||
ратифицированного единого пере-снапшота (D39.62 Q3, D39.190 п.4 ✔), тем же окном, что перекрой глав.
|
||||
- **Носитель:** строка 115 ✔ (триггер «первая новая целевая пара» — почти наступил), D39.64, D39.62 Q3.
|
||||
|
||||
### Д5. Re-pin предикат знает только банк: правка $0-гейта не пере-пиньится, очередь строки 182
|
||||
|
||||
- **Улика ✔:** `repin.go:54` — одно поле; `:61-91` — любое другое расхождение → `moveOther`;
|
||||
`snapshot.go:447` `StyleCheckVersion: checks.CheapGateVersion` — версия правил, которые по собственному
|
||||
объявлению «never changes a chunk disposition» (`cheapgates.go:23-27`, агент), фолдится в денежный хеш.
|
||||
`verdictSnapshotID` — 6 упоминаний, все в комментариях, 0 определений ✔. Консервативность НАМЕРЕННА и
|
||||
прибита: `TestRepinRefusesAnyMoveThatIsNotBankOnly` (`miningstop_join_test.go:1686` ✔); D15/D20 (агент):
|
||||
all-or-nothing приемлем для статичной книги Ф1.
|
||||
- **Арифметическая поправка опровергателя (сверена ✔):** `buildSnapshotID` маршалит ОДНУ структуру и берёт
|
||||
ОДИН sha256 (`snapshot.go:499-503`) — провод и вердикт лежат в одном объекте, перекрой даёт ОДИН новый id и
|
||||
ОДНУ перекупку. Довод пробы «пак заплатит дважды» — ложен.
|
||||
- **Цена:** не доллары (потолок $127/книга на 1500 глав, редактор $114 — `PROGRESS.md:1190`, research/30:157
|
||||
✔; платная книга одна), а **строка 182** ✔ — норма копить, и очередь за ней (§2.А.1). Бампы кучкуются на
|
||||
разработке гейтов: строка `CheapGateVersion` менялась 25.07/26.07/01.08/01.08, `SanitizerVersion` — последний
|
||||
01.08; с тех пор ноль ✔.
|
||||
- **Вердикт: развязать точечно — но НЕ наивной формой.** ⚠ Моя поправка к сводному вердикту воркфлоу
|
||||
(«превратить константу в множество с четырьмя вердикт-ключами»): `resumeFromChunkStatus` отдаёт
|
||||
СОХРАНЁННЫЕ `Disposition`/`FlagReason` (`resume.go:24-26` ✔), пере-классификации в этой ветке нет.
|
||||
Объявить `sanitizer`/`coverage`/`style_check_version`/`postcheck_gate`/`classifier_version` re-pinnable без
|
||||
пере-вынесения вердикта = отдавать за $0 вердикты СТАРОГО алгоритма — ровно «HOLE (b)» спеки D15.2 §6
|
||||
(агент) и тот класс «тихой пере-оценки на резюме», против которого снапшот построен. Честный дизайн —
|
||||
**вердикт-ось с $0-переклассификацией сохранённых выходов** (детерминированные гейты пере-гоняются по
|
||||
тексту чекпойнта, потом ре-пин); это проектирование (строки 49, 124 ось (в) ✔), не правка белого списка.
|
||||
(Пост-чек глоссария — исключение: он пересчитывается каждый прогон и в `chunk_status` не хранится — снятая
|
||||
находка §7.14.)
|
||||
- **Двигает ли:** сам предикат — нет (payload не меняется). Но любое РАСЩЕПЛЕНИЕ полей payload (например
|
||||
`memory_version` на банк и алгоритм) — смена формы = `moveOther` для всех книг в полёте ⇒ только внутри того
|
||||
же resnapshot-окна (§7.14).
|
||||
- **Носитель:** строки 49, 124, 182 ✔; D15, D20; D39.31 сознательно не строил (агент).
|
||||
|
||||
### Д6. `structure_version` вне снапшота — зарегистрированная «дыра Р6»; лечение из пробы дороже зарегистрированного
|
||||
|
||||
- **Улика ✔:** `Structure: r.structure.Fingerprint()` есть в `cutInputs` (`manifest.go:265`) и в ключе
|
||||
манифеста (`:367`); в `snapshot.go` слова `structure` — 0 хитов (контроль: соседи `LangpackVersion`,
|
||||
`EmbeddedVersion` там на месте, `:442-443`); `Fingerprint()` во всём `internal/` — определение + 2 прод-вызова
|
||||
+ 3 теста ✔. `rebill.go:33-36` сам пишет: «WHAT IS NOT PROJECTED (documented, not silent): the CONTENT
|
||||
axis». `--resnapshot` сравнивает `job.SnapshotID` (`stagerun.go:53`, агент) — снапшот не двигается.
|
||||
Латентно: `structure.txt` в дереве 0 файлов (`configs/` ✔; всё дерево 44 559 файлов — агент); грамматика
|
||||
идёт из `DefaultCJKStructure`, выведенной из `cjk-section.txt`, чьи байты в `EmbeddedVersion` — **сегодня
|
||||
правка грамматики громкая**. Дыра открывается первым же шагом штатного пути «a source absent from this
|
||||
repository needs a directory, not a Go change».
|
||||
- **Цена:** после появления `structure.txt` правка грамматики двигает границы → `ch.Text` и плотные номера →
|
||||
`msgsContentHash` расходится → платный цикл БЕЗ `--resnapshot` и БЕЗ сметы согласия. Порядок: ~$9–11 на
|
||||
500 глав (строка 185 ✔). Разработку не тормозит: обходов вокруг дыры в коде не наросло (6 хитов
|
||||
`Fingerprint()` всего ✔).
|
||||
- **Вердикт: развязать точечно** — зарегистрированное лечение (строка 160 ✔, research/27:73 ✔): read-path-
|
||||
расширение `projectRebill` на контентную/позиционную ось — ПОКАЗЫВАЕТ число, не заставляя платить. Плюс один
|
||||
явный пункт в дизайн-пак: `structure_version` не фолдится ни в снапшот, ни в `Pack.Version()` (агент) —
|
||||
решить, видит ли консент-гейт правку грамматики как отдельное событие.
|
||||
- **⛔ Лечение из пробы («одна строка `Structure` в `buildSnapshotID`») — НЕВЕРНОЕ и дороже:** `repinnable()`
|
||||
отвечает `true` только на bank-only move (`repin.go:305-318` ✔), быстрый путь резюма требует
|
||||
`ContentHash` и (`SnapshotID` совпал ИЛИ repinnable) (`stagerun.go:91-92` ✔) ⇒ внеси `Structure` в снапшот —
|
||||
и правка грамматики протухает ВСЕ строки `chunk_status` книги, включая главы, чей текст не двинулся ни на
|
||||
байт: адресная перепокупка становится полной.
|
||||
- **Двигает ли:** read-path — нет.
|
||||
- **Носитель:** строка 160 (отложена ПОРЯДКОМ — D39.190 п.2 ✔), 161, 185 ✔; D39.136 п.3 ✔ (сначала дизайн-пак).
|
||||
|
||||
### Д7. Пар-данные нельзя завезти по частям: манифест требует ≥1 канал, оба канала — китайской формы
|
||||
|
||||
- **Улика ✔:** каналов два, закрыты в Go (`langpack.go:108-114`: `source-morphology`, `transliteration`);
|
||||
`:631-632` `manifest declares no channel — omit the file to ship all channels, or list at least one`;
|
||||
`validate()` при канале `transliteration` требует ПОИМЁННО пиньинь-категории `palladius/initials, finals,
|
||||
yw, special_i` + `phonotactics/…` (`:519-527`). Манифест — файл ЯЗЫКА-ИСТОЧНИКА (`<root>/<src>/manifest.txt`,
|
||||
⟲; проба воркфлоу писала «langpack manifest» — форма та же). Отказ жёсткий: `runner.go:349-352` ✔ — при
|
||||
`gates.terminology.enabled` и `pack == nil` прогон отказывает (`RefusalBadConfig`); в боевом
|
||||
`pipeline-c1.yaml:151` `enabled: true` ✔.
|
||||
- **Замер ⟲:** `ko/manifest.txt` из одного комментария + `ko-ru/heading.txt` → ровно заявленная ошибка;
|
||||
пустой корень + `ko/structure.txt` → `LoadSourceStructure` грузится, `markers=["제"]`. Агент воркфлоу: 11
|
||||
таблиц по ОДНОМУ выдуманному токену — пак грузится (ровно запрещённый воркэраунд).
|
||||
- **Цена:** ни разу не оплачена (0 манифестов ✔, одна пара ✔). Наступает один раз — на второй паре на c1:
|
||||
либо фиктивные данные китайской формы, либо прогон без банка терминов (продуктовая цель № 1). **К перекрою
|
||||
глав отношения не имеет:** несущая половина — разрез — живёт вне канала (`lang.LoadSourceStructure`,
|
||||
`structure.go:142` ✔) и под тестом; за гейтом остаётся только шаблон `heading.txt`, инертный по построению.
|
||||
- **Вердикт: развязать точечно** — разрешить нулевое число каналов (`langpack.go:631-633`, ~5 строк) внутри
|
||||
пака второй пары. ⚠ **Крупнее и точечной правкой не закрывается:** категории каналов зашиты в китайскую
|
||||
форму — пара, которой нужна СВОЯ транслитерация, выразить её не может. Носителя в бэклоге нет.
|
||||
- **Двигает ли:** нет. Байты манифеста фолдятся в хеш ТОЛЬКО при наличии файла (`langpack.go:244-253` ✔),
|
||||
присутствующих манифестов 0 ⇒ `Version()` zh-ru не движется; голден пинит путь БЕЗ langpack (строка 41 ✔).
|
||||
- **Носитель:** нет; D39.64 П4 ратифицировала ОБРАТНОЕ намерение («источник без пиньинь-морфологии грузится
|
||||
без Palladius-файлов») — строка `list at least one` не дошла один шаг до собственной цели (агент).
|
||||
|
||||
### Д8. Номер главы несёт три семантики на одном int — связка реальна, цена подписи завышена на порядок
|
||||
|
||||
- **Улика ✔:** позиция — `migrate.go:126` PK `chunk_status`, `:38` `jobs UNIQUE`; время — `spoilerBlocked`
|
||||
(`memory.go:678-686`); граница — `chapterNo++` (`chunker.go:140`), sticky-reset (`wave.go:84-86`, агент);
|
||||
ключ банка — `TermID(src, sense, since, until)` хеширует `strconv.Itoa(since)` (`decisions.go:166-173`).
|
||||
Радиус — 183 площадки, распределение в §2.Б.3 ✔.
|
||||
- **Замер:** термин с `since_ch=0` инвариантен к пере-нумерации (агент, `-overlay`-тест). На реальном артефакте
|
||||
прогона (`books/…/guzhenren-coldrun-a.db.mined-signature.yaml`, только чтение ✔): **142 термина, 128 с
|
||||
`since_ch: 0`, окно у 14**. В авторских сидах владельца ✔: `guzhenren-seed.yaml` (49 терминов) — 3 ненулевых
|
||||
`since_ch` + 1 `until_ch`; `-v2.yaml` (58) — 3 + 1. Личная плата владельца — ~8 чисел, не «все подписи».
|
||||
- **Гейты уже стоят:** `manifestChapterID` = SHA-256 по ингестированному тексту (`manifest.go:197-215`,
|
||||
агент), `TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere` (`contractblockers_test.go:245` ✔);
|
||||
`TestEveryCutInputMovesTheTag` (`cuttag_test.go:26` ✔) — рефлексией по структуре, перекрой ГРОМКИЙ;
|
||||
перенос окон — класс `moveBankOnly` (research/27:84 ✔ «bank-only move — репиннимый класс»).
|
||||
- **Цена:** сегодня — нет улик, что платится; батарея зелёная, точка применения узкая (3 вызова `Bank.Select`,
|
||||
5 `spoilerBlocked`, 14 файлов с `SinceCh/UntilCh` — агент). Мультиязычность не задета: ординал языконезависим.
|
||||
- **Вердикт: развязать точечно** — формула `Chapter.ID` + окна банка на chapter-ID, внутри дизайн-пака 161
|
||||
(D39.136 п.3 ✔). Переписывать строго дороже: перенос окон при неподвижном крое = `moveBankOnly` = $0-репин;
|
||||
переписывание двигает сам крой = `moveOther` = полная перепокупка.
|
||||
- **Двигает ли:** окна банка → `moveBankOnly` ($0 для всего, чего правка не достаёт); развязка чанкера от глав →
|
||||
крой → `moveOther` ⇒ то же окно перенарезки, что Д4.
|
||||
- **Носитель:** строки 161, 298 ✔; контракт 14 `StructureVersion` (агент); D39.122 п.2(е) форма `unit.id`.
|
||||
|
||||
### Д9. Runner — шина на 39 полей: цена платится не за размер, а за непиненный порядок предвычисления
|
||||
|
||||
- **Улика ✔:** `runner.go:42` — 39 полей; 178 методов в 29 файлах; рост 16 → 25 → 39 (§1). Каталог мутаций:
|
||||
`bookrun.go`/`seeding.go`/`snapshot.go`/`bankexport.go`/`repin.go` — 0 из 211; у `mining.go` 8, но ни одна не
|
||||
про порядок экспорта (FX23 — «map-before-memory»); `Makefile:58` без `tmmutate`. Порядок в `bookrun.go` ⟲:
|
||||
`seedGlossary` :174 → `exportBank("run-start/seeded")` :180 → `SplitChunksWithChapters` :182 →
|
||||
`persistManifest` :194 → `r.bankSrc` :198 → `precomputeSticky` :207 → `planVolume` :219 →
|
||||
`checkRebillConsent` :229 → `translateBookWaves` :242. `exportBank` читает СТОР, не шину
|
||||
(`bankexport.go:138` `r.Store.GlossaryForBook` ✔).
|
||||
- **Замер ⟲:** мутация Б (`seedGlossary` за консент-гейт, вплотную к волнам) → **ровно 15 красных**, среди
|
||||
них `TestGoldenDeterminism`, `TestRebillProjectionIsPerWave`, `TestSingleSystemEndpointCarriesTheBankOnBothWaves`,
|
||||
`TestABoundedRunCannotOutrunTheConsentThreshold` — денежный порядок держится, и громко. Мутация А
|
||||
(`seedGlossary` за `exportBank("run-start/seeded")` и `r.bankSrc`, до `precomputeSticky`) → **батарея
|
||||
полностью зелёная**, при этом «run-start/seeded» публикует банк ПРЕДЫДУЩЕГО прогона. Порядок пиненный
|
||||
ровно на денежном шве и голый на шве $0-проекций.
|
||||
- **Счета уже уплачены дважды:** строка 257 ✔ — тот же класс, найден мутационной пробой, СЕГОДНЯ закрыт пинами
|
||||
(`volumeslot_test.go:866/:932`, агент: WB60/WB64/FX23 → RED); строки 224/253 ✔ — ОТКРЫТЫ (`mining.go:269`
|
||||
вне `if !stopping`, §2.А.3), срок «до первого внешнего пользователя».
|
||||
- **Что заявлено и НЕ подтвердилось:** «25 площадок присваиваний в 13 файлах» — 22 в 11 вне `runner.go`, греп
|
||||
ловил чужой ресивер `sr.` (агент); «read-path пишет в шину» (`status.go:456`) — натяжка: «pure read» там про
|
||||
стор, не про мемоизацию (агент). Шина — объявленный композит-корень (D39.23, тело в архиве ✔;
|
||||
`03-implementation-notes.md:5`), толщина уже была замерена и использована как довод за процессный шов
|
||||
(D39.81, агент).
|
||||
- **Вердикт: развязать точечно — часы, не пак.** Записи в `mutations.json` на порядок предвычисления
|
||||
(`bookrun.go`, `mining.go`); `tmmutate` (или его порядковое подмножество) в `make battery`; закрыть строку 224
|
||||
(экспорт на стопе — ПОСЛЕ наполнения банка). Это тот же механизм, которым проект закрыл строку 257.
|
||||
- **Двигает ли:** нет — тесты и содержимое артефакта, не провод.
|
||||
- **Носитель:** строки 224/253/313 ✔ (313: «каталог доказывает, что мутация существует, а не что она ловится»).
|
||||
|
||||
### Д10/Д11. Шов «заголовок главы» не защищён ничем: три мутанта переживают всю батарею, под одним из них книга теряет все названия глав
|
||||
|
||||
- **Улика ✔:** `chunker.go:145` `chapterChunks[0].Heading = headingText`; `manifest.go:664` `if ch.ChunkIdx == 0`
|
||||
(восстановление заголовка на read-пути манифеста — `tmctl export`); `export.go:174/:340/:341`
|
||||
`u.Members[0].Heading`; `bookbuild.go:526-527` подставляет НОМЕР, когда `Heading == ""`, `:536` `TrimPrefix`.
|
||||
Тесты с правильными именами: `TestSplitChunksHeadingCarried` (`chunker_heading_test.go:101`) обещает «FIRST
|
||||
chunk only», негативной проверки нет; `TestManifestChunksReproduceTheCut` (`contractblockers_test.go:294`) —
|
||||
комментарий `:298` «A long chapter so the cut produces several chunks» ✔.
|
||||
- **Замер ⟲:** обе фикстуры дают **1 чанк на главу** (`perChapter=map[1:1 2:1]`; в чанкерной 2 непустых
|
||||
заголовка, в манифестной `nonEmptyHeadings=0` и в разрезе, и в реконструкции — сравнение `Heading` там есть
|
||||
`"" == ""`; «длинная» глава в 5159 байт даёт 1 чанк / 1 юнит). Мутанты M1 (`if i == 0` — заголовок на первый
|
||||
чанк КАЖДОГО юнита), M2 (`if false`), M5 (`chapterChunks[len-1]`) и M3 (`export.go:340`, а также все три
|
||||
места 174/340/341 разом) — **каждый переживает полную батарею, 18 ok / 0 FAIL**. Под M2 выданная книга
|
||||
(`TranslateBook → Export → BuildBook`) несёт «1», «2» вместо «Глава 1», «Глава 2» (агент; механизм
|
||||
подтверждён чтением `bookbuild.go:526-527` ✔ — номер подставляется при пустом `Heading`); под M1 «Глава 1»
|
||||
повторяется в середине главы в артефакте `tmctl export`, который читает платформа (агент). На реальной
|
||||
книге 300 из 300 глав многочанковы (239×2, 57×3, 4×4) — набор упражняет случай, которого в проде нет (агент; главы резались по маркерам `第N节`, не `\f`-препаратом — оговорка агента).
|
||||
- **Поправки опровергателя (важны, чтобы не чинить здоровое):** боевой код КОРРЕКТЕН — `ChunkIdx`
|
||||
сбрасывается на 0 в каждой главе, `if ch.ChunkIdx == 0` ≡ «первый чанк главы» по построению, round-trip на
|
||||
невырожденной фикстуре 0 расхождений из 8; связь глава→чанки→edit-unit ЗАЩИЩЕНА (контрольная мутация
|
||||
«каждый чанк = свой юнит» убита 9 тестами; 27 тестов дают главе >1 чанка) (агент). Не защищено ровно одно —
|
||||
РАСПОЛОЖЕНИЕ заголовка; за все 570 разрезов батареи `headingOnNonFirst = 0`, но это свойство кода, не
|
||||
утверждение теста; упоминаний `Heading` в тестах 41–48 в 5–7 файлах (счёт зависит от популяции) — ни одно
|
||||
про не-первый чанк; единственные проверки пустого заголовка — индекс 0 при nil-правиле
|
||||
(`chunker_heading_test.go:130`) и то самое `"" == ""` (`contractblockers_test.go:316`) ⟲.
|
||||
- **Вердикт: развязать точечно, ДО дизайн-пака.** (а) обеим фикстурам — главу, которая реально режется, и
|
||||
правило заголовка zh-ru; (б) утверждение «не-первый чанк главы несёт пустой `Heading`»; (в) M1/M2/M5 в
|
||||
`mutations.json`. Доказано исполнением: тело утверждения байт-в-байт + другая фикстура → под HEAD `ok`, под
|
||||
M1/M2 `FAIL` (агент). Класс назван проектом вчера — D39.208 п.5 ✔ («фикстура, в которой две различные
|
||||
величины совпадают, делает весь их класс расхождений НЕВИДИМЫМ»); последний коммит по обоим тест-файлам
|
||||
`2f65d1d` (06.09) ✔ добавил 134 строки в `mutations.json` и оставил обе фикстуры вырожденными. В каталоге ни
|
||||
одна правка не содержит `chapterChunks`, `ChunkIdx == 0`, `.Heading`, `Members[0]`, `ApplyHeading` ⟲.
|
||||
- **Двигает ли:** нет — боевой код не трогается.
|
||||
- **Носитель:** нет. `chunker.go` в каталоге мутаций отсутствует вовсе ✔.
|
||||
|
||||
---
|
||||
|
||||
## 4. Большой перекрой глав: дизайн-пак, не переписывание
|
||||
|
||||
Довод не вкусовой, а денежный и посчитанный проектом раньше этого замера: research/27 §6 п.2 ✔ — любой новый
|
||||
детектор границ бампает `chunkerVersion`, двигает ОБА волновых снапшота, `$0`-репин не работает
|
||||
(`repin.go:61-91`), перекупаются обе волны на 100 %; `golden_test.go:23-29` ✔ пинит провод побайтово ровно
|
||||
потому, что сдвинутый `request_hash` = вся книга заново. Переписывание покупает ноль и стоит ровно того, чего
|
||||
владелец просил избежать. Под мандатом D39.216 ✔ («где форма не тянет — переписывать; „дешевле подпереть“
|
||||
доводом не является») развилка выглядит так:
|
||||
|
||||
**Спроектировать ЗАНОВО и отрефакторить без заплат (форма не тянет):**
|
||||
|
||||
- **Формула `Chapter.ID` + развязка чанкера от глав** (строка 161). Единственный пункт, который заплатой закрыть
|
||||
нельзя: 183 площадки, 64 % в membank+store. Половина фундамента стоит и запинена (`manifestChapterID`,
|
||||
`TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere`). Сюда же — **два числа главы**: плотный `Chapter`
|
||||
(PK, адрес redrive) и номинал в заголовке (`Heading` рендерит исходное `n`) уже расходятся на неплотной
|
||||
нумерации и на прологе ⟲ — дизайн обязан назвать, каким числом адресуется глава.
|
||||
- **Банк-окна `since_ch`/`until_ch` → chapter-ID** (`decisions.go:166-173`, `memory.go:678-686`). Класс переноса
|
||||
`moveBankOnly` = $0-репин; миграция мелкая (14 из 142 добытых, ~8 чисел в сидах ✔).
|
||||
- **Предикат осей `classifySnapshotMove` + вердикт-ось с $0-переклассификацией** (Д5). Сегодня — одна строковая
|
||||
константа; обязан стать явной картой осей (провод / вердикт / банк / контент / позиция) с честным ответом,
|
||||
что делать на каждой: провод — купить, вердикт — пере-вынести из чекпойнта за $0, банк — ре-пин, контент —
|
||||
показать смету. От него зависит и очередь 182, и то, будет ли перекрой молчаливым.
|
||||
- **`projectRebill` — контентная и позиционная оси** (Д6, дыра Р6) — одним куском с предикатом выше: две стороны
|
||||
вопроса «какая ось уехала».
|
||||
- **Расщепление `EmbeddedVersion` на три радиуса** (Д4) — по образцу `bankdata.go`; сам ремонт двигает хеш ⇒
|
||||
только внутри окна перенарезки.
|
||||
- **`structure_version` как самостоятельная ось** консент-гейта — решить в дизайне; ⛔ НЕ через
|
||||
`buildSnapshotID` (Д6).
|
||||
- **Правило заголовка** — три предиката свести к одному источнику стражей и завести паритет-тест ингест↔чанкер
|
||||
(Д3); ось юнита — по D39.207 в пак 2.
|
||||
- **`unit_resolutions` платформы** (строка 298) — аддитивно, но в ТОМ ЖЕ дизайне: разрешать `(chapter, unit)` в
|
||||
идентификатор при приёме события (D39.202, агент).
|
||||
|
||||
**Оставить и достроить (переписывать нечего):**
|
||||
|
||||
- **`matchHeaderLine` + `parseSectionNumeral`** — research/27 §6 ✔ прямо оставляет их в роли готовой
|
||||
грамматики / первого паттерн-пака индуктора.
|
||||
- **`Runner` как композит-корень** — объявленное решение (D39.23, статус «частично»), не недосмотр. Перекрой
|
||||
трогает в шине микроскопическую поверхность: `r.structure` 5 ссылок в 3 файлах, `r.cutStructure` 2,
|
||||
`r.bankSrc` 4 (агент). Денежный порядок держат 15 тестов и голден ⟲.
|
||||
- **`ThinksOnWire`/`echoMineViolation`** (Д2) — полдня, ВНЕ окна перекроя, снапшот-нейтрально.
|
||||
- **Нулевой канал langpack** (Д7) — пятистрочная правка внутри пака второй пары; к главам не относится.
|
||||
- **Словарь классификатора** (Д1) — линт в существующем файле, $0.
|
||||
- **Шов заголовка** (Д10/Д11) — фикстуры и мутации, боевой код цел.
|
||||
|
||||
---
|
||||
|
||||
## 5. Порядок развязки — что первым, чтобы остальное подешевело
|
||||
|
||||
**Шаг 0 — часы, $0, снапшот не двигают, ДО дизайна.** Иначе дизайн принимается без обратной связи ровно там,
|
||||
где режут:
|
||||
|
||||
1. Невырожденные фикстуры `chunker_heading_test.go` / `contractblockers_test.go` + утверждение о пустом
|
||||
`Heading` не-первого чанка + M1/M2/M5 в `mutations.json` (Д10/Д11).
|
||||
2. Мутации на порядок предвычисления `bookrun.go`/`mining.go` + `tmmutate` в `make battery` + закрытие строки
|
||||
224 (Д9).
|
||||
3. Линт словаря классификатора по канонической форме промта в `prompt_lint_test.go` + `bankTypeOK` →
|
||||
`CandidateTypes` (Д1).
|
||||
4. `ThinksOnWire` → делегация в `Capability.SuppressesThinking` (Д2) — снапшот-нейтрально, в любое окно.
|
||||
5. ⚠ **По Д3 в шаг 0 идёт ЗНАНИЕ, не фикс:** паритет-тест ингест↔чанкер на `第零章`/85-рунной строке сегодня
|
||||
КРАСНЫЙ, а его зелень требует правки чанкера, которая по дисциплине бампает `chunkerVersion` (§3 Д3). Тест
|
||||
пишется сейчас, приземляется вместе с фиксом в шаге 3. (Сводный вердикт воркфлоу клал фикс в шаг 0 — здесь
|
||||
расхождение с ним, довод в Д3.)
|
||||
|
||||
**Шаг 1 — первым в дизайне: предикат осей** (`classifySnapshotMove` + `projectRebill` + вердикт-ось с
|
||||
переклассификацией). Он удешевляет всё остальное двумя способами: снимает норму «копить одним касанием» (то
|
||||
есть распечатывает очередь 182, включая $0-гейтовые правки) и превращает перекрой из «молчаливая перекупка» в
|
||||
«названная и согласованная». Вся обвязка вокруг него стоит и покрыта (1079 строк ✔, 16 тестов).
|
||||
|
||||
**Шаг 2 — расщепление `EmbeddedVersion` по трём радиусам** — ТЕМ ЖЕ окном, что перенарезка: ремонт сам двигает
|
||||
хеш, отдельным актом он платит цену, которую отменяет (D39.62 Q3, D39.190 п.4).
|
||||
|
||||
**Шаг 3 — `Chapter.ID` + развязка чанкера от глав + перенос банк-окон + сведение правила заголовка (с фиксом
|
||||
`第0章` и стража длины)** — `moveBankOnly` для окон, одна перенарезка для остального.
|
||||
|
||||
**Шаг 4 — аддитивно:** контракт 14, `unit_resolutions` платформы, фронтовая половина строки 160.
|
||||
|
||||
**Ключевое: шаги 2 и 3 — ОДНА перенарезка. Разнести их = заплатить дважды.** Единственное дешёвое окно —
|
||||
пока платная книга одна (строка 161: «окно закрывается первой книгой внешнего пользователя»).
|
||||
|
||||
---
|
||||
|
||||
## 6. Чего делать НЕ надо — где рефакторинг дороже, чем терпеть
|
||||
|
||||
- **Не переписывать `Runner` и не дробить `internal/pipeline` дальше «ради здоровья».** Норматив
|
||||
`12-go-style-notes.md` §1: размер — не критерий сплита; D39.23 сплит по измеренной связности исполнен, остаток
|
||||
— композит-корень. Перенос `seedGlossary` за консент-гейт валит 15 тестов ⟲ — денежный порядок держится;
|
||||
64 % радиуса перекроя лежит в membank+store ✔; переписывание шины двигает снапшот-байты.
|
||||
- **Не вносить `Structure` в `buildSnapshotID`** «одной строкой» — адресная перепокупка становится полной (Д6).
|
||||
- **Не расширять белый список `classifySnapshotMove` вердикт-ключами как «быстрый выигрыш»** — без
|
||||
переклассификации это отдача стухших вердиктов за $0 (Д5, `resume.go:24-26`).
|
||||
- **Не строить детектор границ с нуля** — research/27 §6 п.2: бамп `chunkerVersion` → 100 % обеих волн.
|
||||
- **Не трогать ось «обязательность юнита» вне пака 2** — D39.207, строка 303.
|
||||
- **Не менять байты промтов, словарей и `classifierVersion` ради стройности** — D39.194 п.9, D39.199 п.5.
|
||||
- **Не втаскивать снапшот-нейтральные мелочи (Д1, Д2, Д7) в окно перекроя** — им там не место, они дешевле
|
||||
отдельно. И симметрично: **не откладывать шаг 0 «до пака»** — он $0 и он условие качества самого дизайна.
|
||||
- **Не копить wire-правки дальше без шага 1** — копление и есть цена; после предиката осей оно перестанет быть
|
||||
нужным.
|
||||
- **Не заводить второго носителя** там, где опровергатель показал один источник: `res.Flagged` кормит exit-код,
|
||||
`finished.outcome` и `CompletedWithFlags` из одного счётчика (§7.11); словарь событий движок↔платформа
|
||||
аддитивен по построению (§7.16).
|
||||
|
||||
---
|
||||
|
||||
## 7. Что снято опровергателями — с доводом
|
||||
|
||||
Двадцать находок проб пали на проверке цены. Каждая — с решающим доводом, чтобы следующая смена не искала
|
||||
заново; побочные РЕАЛЬНЫЕ мелочи, которые опровергатели подняли попутно, помечены «остаток». ⚠ Доводы этого
|
||||
раздела — из отчётов опровергателей (каждый мерил на копии дерева исполнением); мной сверены только адреса с
|
||||
меткой ✔. Опровергатели работали на четырёх HEAD одного дня с побайтово равным `backend/` (шапка).
|
||||
|
||||
1. **«StatusReport → render.go → golden: триада, через которую проходит каждая фича» — ОСТАВИТЬ.** Нога
|
||||
«golden» ложна: `golden_test.go` не упоминает `StatusReport`/`.Status()` (0 хитов), добавление поля в
|
||||
`StatusReport` голден тронуть не может; `renderStatusJSON` — голый `json.Encoder`, новое поле на машинном
|
||||
пути стоит ноль; платформа читает `status --json` по аллоулисту на 11 полей. D39.181 ратифицировал класс
|
||||
«новое поле контракта шва». Гейт стоит там, куда перекрой садится: `TestEveryCutInputMovesTheTag`.
|
||||
2. **«История уже оценила перекрой: 13 файлов, 5 пакетов, 0 миграций» — не долг.** Правильный ответ владельцу
|
||||
(дизайн-пак), но цены не называет: вторая улика (`0e69bc1`) раздута ~4.5×; «ноль миграций» — потому что оба
|
||||
коммита ординал не двигали. Настоящая цена — бамп `manifestVersion` (`manifest.go:709-711`), и она уже
|
||||
погашена нормой: `TestTheManifestDocumentVersionDoesNotMoveForAnAdditiveField`.
|
||||
3. **«Две идентичности главы: int в БД и контент-хеш в манифесте» — ОСТАВИТЬ.** Шифт-мап пака структуры глав
|
||||
(`backend/docs/CHAPTER_STRUCTURE_SHIFTMAP.json`): 13 стенд-книг, `same=2355, moved=0`, ключ манифеста
|
||||
пере-минтился на 13/13 — форма двинулась, ни одна идентичность не переехала, $0. Две оси — ратифицированное
|
||||
различение (D39.122 п.2(е)), моста между ними нет (`manifestChapterID` только в `manifest.go`), гейт против
|
||||
молчаливой перепокупки стоит (`TestTheCONSENTGateSeesAnInPlaceSourceEdit`, `rebillsource_test.go:138` ✔).
|
||||
Проба перепутала, где дорого: сдвиг границ = вся книга, перенумерация = хвост.
|
||||
4. **«internal/pipeline — 17 тыс. строк, 31 % имён протекает между файлами» — ОСТАВИТЬ.** 31 % — МЕДИАНА репо
|
||||
(store 47 %, miner 38 %, config 31 %); метрика меряет «это Go-пакет». Перекрой 06.09 (`2f65d1d` и соседи)
|
||||
принёс в `internal/pipeline` 138 строк в 6 файлах против 1197 в `chunk` и 261 в `lang` — семантика главы
|
||||
ЛОКАЛИЗОВАЛАСЬ в пакеты, созданные сплитом D39.23. `LoadPipeline` 353 стр. — другой пакет (`config`).
|
||||
Остаток: время прогона каталога мутаций (126 записей × 71 с) — строка 313, D39.214.
|
||||
5. **«Стабильный chapter-ID движок не читает» — ОСТАВИТЬ.** Читает ПЛАТФОРМА, для которой он построен
|
||||
(`platform/internal/ingest/manifest.go:124-127`, `books.ordered_through_chapter_id`, D39.208); внутри
|
||||
движка ординал — ратифицированная граница (D39.122 п.2(а)). Компилятор-пробы: смена типа даёт 25/10 и 44/14
|
||||
ошибок, 100 % в одном пакете — подпись здоровой границы. Перевод окон банка на хеш невозможен по смыслу
|
||||
(`spoilerBlocked` — порядковый предикат; контракт 14 README:343).
|
||||
6. **«Чанкер принимает решения четырёх слоёв в одной функции» — ОСТАВИТЬ.** Композиция, не клубок: три из
|
||||
четырёх решений за своими швами (`stripHeading`, `assignEditUnits`, `chapterDraftChunks`), инлайном — две
|
||||
строки нумерации. Перекрой 06.09 взял с функции ~10 механических строк. Остаток → Д3 (паритет нуля) и
|
||||
«два числа главы» → §4.
|
||||
7. **«status.go: 1183 строки, read-model и деструктивная команда на одном получателе» — ОСТАВИТЬ.** Перекрой
|
||||
`2f65d1d` тронул `status.go` одной строкой в 717 строках от `ResetChunkStages`; дизайн-док перекроя
|
||||
`status.go` не называет; редрайв под тремя мутациями падает четырьмя гейт-тестами (D17 п.5(в)(г) исполнены).
|
||||
Норматив против разреза по размеру. **Остаток (доки):** `backend/README.md` называет `status.go` read-only
|
||||
проекцией — ложь, там единственный деструктивный путь; `status_test.go:376` обещает замок, который в одиночку
|
||||
не держит (с убитым гардом тест проходит, потому что первым отказывает rebill-гейт).
|
||||
8. **«Новая ЦЕЛЬ требует правки Go (`//go:embed`) и пере-снапшотит все пары» — ОСТАВИТЬ.** Механизм точен, цена
|
||||
не платится: гейт ловит громко (`TestGoldenDeterminism` красен ровно один из 21 пакета), событие не
|
||||
происходило ни разу (4 правки строки `embed`, все до 01.08 ✔), забуковано строкой 115 с триггером, соседняя
|
||||
форма разобрана владельцем в D39.62 Q2/Q3. Условный фолд измеренно снапшот-нейтрален
|
||||
(`embed-v1-3df4329e3b9e` байт-в-байт). Брать одним касанием со строкой 120 при второй цели. → влилось в Д4.
|
||||
9. **«Ингест не режет по безъюнитной грамматике» — ОСТАВИТЬ.** Недостижимо данными (0 `structure.txt` на 44 559
|
||||
файлов); закрыта оговорка строки 303 — три живые книги владельца через настоящий `IngestEncoded`: EPUB
|
||||
32/71 глав (declared), плоский en-txt — 1 глава, и заявленная правка на нём НИЧЕГО не меняет (заголовки
|
||||
словесными числительными в блоке оглавления, в теле их нет). Правка 24 строки, батарея зелёная, но делать
|
||||
СЕЙЧАС хуже, чем потом: ветка без корпуса для замера точности. ⚠ Агент честно сообщил заражение своей
|
||||
копии от соседней сессии (общий scratchpad) — перемерено в приватном каталоге.
|
||||
10. **«Алфавит майнера зашит в `unicode.Han`» — ОСТАВИТЬ.** Ратифицированная незастроенная фича с номером
|
||||
(D39.50 п.8, строка 35): нужен ВТОРОЙ детектор, не правка этого; молчание не тихое (пак без данных падает
|
||||
громко, `TestEmissionFunnelIsReported`). Развязка измерена: 50 строк, зелено, `Pack.Version()` не движется —
|
||||
но покупает ноль (en по-прежнему 0). Второй хардкод — `allCyrillic` в `miner_palladius.go:128`. ⚠
|
||||
`TestMinerFullBookParity` на этой машине скипается (нет jieba-артефакта).
|
||||
11. **«„Чем кончился прогон“ решается дважды: exit-код и `finished.outcome`» — ОСТАВИТЬ.** Поле
|
||||
`finished.outcome` — write-only во всём репозитории: платформа кадр `finished` выбрасывает намеренно
|
||||
(`sink.go:195`), статус берёт из exit-кода; несущая половина загейчена (мутация exit-кода валит 3 теста +
|
||||
голден `exit=2`); источник ОДИН — `res.Flagged`. D39.181 §1 п.2 рассматривал биективность и отверг.
|
||||
Остаток: дешёвая дописка `tracedCtx()` во флаг-фикстуре.
|
||||
12. **«Пять fold-тестов стерегут функцию, которой в проде нет» — снято, форма «развязать точечно».** Волновое
|
||||
разделение защищено `snapshot_wave_test.go` (пропущен грепом пробы): три мутации — 23, 1 и 6 падений. И
|
||||
инверсия: `TestBanknoteSnapshotFold` — ЕДИНСТВЕННЫЙ сторож мутации `TokenBudget` в общем `buildSnapshotID`;
|
||||
удалить «мёртвые» тесты = снять защиту от тихой перепокупки. Остаток: `snapshot.go:206-207/:218`
|
||||
— протухшие клаузы про последовательный драйвер (драйвер один, `bookrun.go:242`).
|
||||
13. **«Пять pipeline-конфигов дублируют настройки; арм-шапки лгут» — снято, «развязать точечно».** Механизм
|
||||
наследования ЕСТЬ и ратифицирован (D39.23 п.(г), `configs/pairs/<пара>.yaml`); `segmentation` — 0 раз в
|
||||
пяти конфигах, живёт в одном пар-файле; эмпирика опирается на пре-регистрированные книго-локальные армы, не
|
||||
на репо-армы. **Остаток — живая мина с деньгами:** шапки трёх армов «ЕДИНСТВЕННОЕ изменение» ложны на
|
||||
11–13 ключей; `STACK.md:12` «флип = один арм-конфиг» молча снимает банкноту + терминолога + классификатор;
|
||||
`bank_contour_test.go` грузит только c1. Лечение — минуты.
|
||||
14. **«`memory_version` — смешанная ось, ре-пин отдаёт стухшие вердикты за $0» — ОСТАВИТЬ.** Опровергнуто
|
||||
исполнением в обе стороны: вердикт пост-чека НИКОГДА не пишется в `chunk_status` (`export.go:250-256`),
|
||||
живёт в `retrieval_state` и пересчитывается каждый прогон — 0 новых вызовов, $0, вердикт пошёл за новым
|
||||
алгоритмом. Ось — D15.2 §6 «HOLE (b)», строка 49. Расщепление поля = смена формы payload = `moveOther` для
|
||||
всех книг в полёте → только в окне 182. Остаток: `snapshotdiff.go:157-159` при движении одного
|
||||
`memory_version` дописывает причину, которой не проверял («the auto-bank grew»).
|
||||
15. **«Денежный контент-хеш снапшота без рефлексивного стража» — ОСТАВИТЬ.** Посадки: удаление
|
||||
`embedded_version` из payload → `TestGoldenDeterminism` красен (56 из 65 json-тегов пинятся голденом);
|
||||
второй рубеж — `ContentHash` на резюме (`rebillsource_test.go:18-22`). Страж в 45 строк пишется без правки
|
||||
Go (payload уже возвращается строкой). Реально не прикрыты два ЛАТЕНТНЫХ слота — `langpack_version`
|
||||
(строка 41) и `escalate_provider_*` (строка 229).
|
||||
16. **«Словарь событий движок↔платформа разъехался на 12 полей» — ОСТАВИТЬ.** 12 = три аддитивных добавления
|
||||
(`volume` 8, `money` 3, `shortfall`); «только у платформы» — 0 из 24; 11 из 12 висят на кадре, который
|
||||
платформа выбрасывает намеренно; 12-е — сознательное не-зеркало с нотой (D39.203). Гейт есть:
|
||||
`TestANewFieldInsideAKnownEventIsIgnored`. `chapters_total`/`units_total` — поля МАНИФЕСТА, а он гейчен
|
||||
жёстко; `unit_done` покрыт эпохой `structure_version`/`shape_epoch` (D39.165).
|
||||
17. **«Конвертер цен написан руками, `CacheWritePerM` выпадает бесшумно» — ОСТАВИТЬ.** «День платной записи в
|
||||
кэш» исполнен на нетронутом коде: 3.75 дошло до леджера, `CostUSD` и `EstimateUSD` считают. Строгий декод
|
||||
`KnownFields(true)` ловит опечатку; ноль — вендор-факт (D39.137). Мутационный пробел на строке, которую
|
||||
никто не правит; закрывается одной фикстурой с ненулевым значением (~28 фикстур пишут явный 0).
|
||||
18. **«Раскладка проекта собрана заново в 6 из 9 setup-хелперов и 13 файлах inline» — ОСТАВИТЬ.** В YAML
|
||||
структуры глав НЕТ (0 ключей из 31); перекрой ведётся компилятором (переименование поля — 11 ошибок с
|
||||
адресами); худший случай (новый обязательный ключ, 407 падений) чинится одним `sed` за 0.013 с; риск
|
||||
перепокупки держит `BriefHash`, не фикстуры. Консолидация размыла бы мультиязычное разнообразие фикстур
|
||||
(D39.208).
|
||||
19. **«Остаточная жёсткая письменность: 29 строк в 10 файлах» — ОСТАВИТЬ.** Хвост ратифицированной карты
|
||||
D39.60 (149 сайтов / 45 файлов), носители — строки 79/35/115; три члена дизъюнкции в `chunker.go:265`
|
||||
мёртвы (IsLetter покрывает); латинный класс уже закрыт `TargetScriptNonLatin()`; до 29 строк дело не
|
||||
доходит — раньше упирается в строку 115. Поправка в пользу пробы: для греческой/арабской цели детектор не
|
||||
нем, а ложно срабатывает — недостижимо, целевых данных одно `target-ru.txt`.
|
||||
20. **«Два шаблона фолда гейта с противоположной ценой, ничто не заставляет выбрать дешёвый» — ОСТАВИТЬ.**
|
||||
Заставляют четыре носителя: D39.60 §6.2 (условный фолд — ратифицированное правило), стиль-норма
|
||||
`12-go-style-notes.md:7`, два зелёных теста, три рантайм-стопа; 3 из 3 гейтов после появления дешёвой формы
|
||||
выбрали её или не фолдятся. Линт отличить «опциональную фичу» от «оси у всех» не может — генератор ложных
|
||||
срабатываний.
|
||||
|
||||
### 7.1. Не оспаривались (косметика, 8 находок проб — носителей нет, цена копейки; всё «(агент)»)
|
||||
|
||||
- Словарь родов — четыре носителя (`memory.go:946`, `memseed.go:595`, `classify.go:50`, промт), и текст
|
||||
собственной ошибки `memseed.go:614` уже врёт о принимаемом множестве.
|
||||
- `zh/numeral.txt` уже эталона `cjk-section.txt` на `〇两兩` — вопрос владельцу: намеренно или недосмотр
|
||||
(потребитель — канал «числительное + ранг-слово» майнера).
|
||||
- `unicode.Version` вплетён в 5 версионных констант, 4 — в нерепинуемые поля: апгрейд Go = перепокупка всех
|
||||
незакрытых книг; stdlib-юникод прибит комментарием, `x/text` — механизмом.
|
||||
- 37 % не-тестовых строк — комментарии (16 048 против 27 585 кода); множитель цены каждого переименования.
|
||||
- «Последняя стадия» двумя одинаковыми инлайнами (`export.go:279-282`, `quality.go:340-343`) при готовом
|
||||
`finalStageWave()`.
|
||||
- Реестр кодировок — закрытый Go-switch (Shift-JIS/EUC-KR требуют case; отказ громкий); резерв токенов
|
||||
банкноты выведен для кириллической цели (для кана-цели занижен ~2.1×, строка 40).
|
||||
- 97 из 200 тест-файлов зовут неэкспортируемые имена — переименование, не форма; в оценку перекроя не идёт.
|
||||
- 6 из 16 никогда не исполняемых функций — мёртвый код (`SafeGo`, `TokenizeCyrillic`, `UnitScaleHard/Soft`,
|
||||
`Voices`, `Pairs`); `SafeGo` с докой про паник-рекавери, которого никто не использует.
|
||||
|
||||
---
|
||||
|
||||
## 8. Чего замер НЕ покрыл — прямо
|
||||
|
||||
- **Скорость разработки как таковая.** Метрики цикла в репо нет; срез инертности мерил rework-rate и недельную
|
||||
выработку по git, а один коммит здесь = один пак целой сессии, так что «файлов на коммит» сцепление НЕ
|
||||
измеряет (агент оговорил сам). Окно замера — 9 недель.
|
||||
- **Долларовая цена перекроя на сегодняшнем корпусе.** Есть только потолок $127/книга на 1500 глав
|
||||
(`PROGRESS.md:1190` ✔) и «100 % обеих волн» из research/27; сметы `projectRebill` по реальной книге под
|
||||
перекрой никто не снимал.
|
||||
- **Не-CJK ингест на живой книге** — покрыт частично одним опровергателем (§7.9: EPUB режется по оглавлению,
|
||||
плоский en-txt — одна глава); словесные числительные и книги без разметки глав в теле — не мерены нигде.
|
||||
- **Мутационное покрытие `prompts/`** — ноль по построению (2 не-Go цели из 211, обе не в `prompts/`) ✔.
|
||||
- **`TestMinerFullBookParity`** скипается на этой машине (нет `eval/exp16/data/`): фрозен-паритет майнера с
|
||||
питоновским эталоном в зелени ни одного агента НЕ участвовал (агент).
|
||||
- **Платформа, фронт, полигон** — не мерились; платформа фигурирует только как потребитель швов.
|
||||
- **Замер Д10/Д11 на реальной книге** (300/300 многочанковых) сделан по маркерам `第N节`, а не `\f`-препаратом
|
||||
пайплайна — распределение может слегка сдвинуться, вывод (даже кратчайшая глава даёт 2 чанка) — нет (агент).
|
||||
- **Рантайм и производительность** — не в предмете; единственное касание — время прогона каталога мутаций
|
||||
(строка 313).
|
||||
- **Сквозные прогоны с фейк-провайдером** (Д1) и `git log -L` по телам правил (Д3) — только у агентов замера,
|
||||
мной не воспроизведены.
|
||||
|
||||
---
|
||||
|
||||
## 9. Реестр сверки — что сверено лично, что агентами, чему доверяю меньше всего
|
||||
|
||||
**Сверено мной по HEAD `e69902a` (✔):** все `file:line` в §3 без метки «(агент)» перечитаны; счётчики файлов,
|
||||
строк, тестов (три популяции), `Runner` (поля/методы/рост по `git show`), каталог мутаций (211/43 и нули по
|
||||
файлам), `Makefile`, 183 площадки ординала с распределением, `mining.go` порядок, 8 коммитов `lang/data` и
|
||||
история строки `embed`, бампы `CheapGateVersion`/`SanitizerVersion`, `TargetChecksFor`, `verdictSnapshotID`,
|
||||
`Fingerprint()`, банк-артефакт и сиды (только чтение), строки бэклога 14/31/35/41/49/114/115/124/160/161/164/
|
||||
182/185/198/224/253/257/298/303/313 и число строк 230, тела D15/D20/D39.23/60/62 Q3/64/81/121/136 п.3/183/
|
||||
190 п.2,4/194 п.9/199 п.5/207/208 п.5/216, research/27 §6 п.2/п.4/«что переиспользуется»/этап 1, батарея на
|
||||
HEAD (EXIT 0), DAG без циклов, импортёры `pipeline`, анализаторы archguard (3), TODO-маркеры (0).
|
||||
|
||||
**Воспроизведено исполнением моими агентами-верификаторами на копиях дерева (⟲):** Д9 — мутации А (зелено) и Б
|
||||
(15 красных, поимённо); Д3 — `第零章`/`第0章` сквозь ингест→чанкер, `parseSectionNumeral` на нуле, обе мутации
|
||||
стража длины (зелено), отсутствие паритет-теста, упоминания в доках; Д7 — ошибка нулевого канала, загрузка
|
||||
`ko/structure.txt` из пустого корня, `runner.go:349-352`, `pipeline-c1.yaml:151`; Д10/Д11 — M1/M2/M3/M5
|
||||
(каждый 18 ok / 0 FAIL), измеренная вырожденность обеих фикстур, отсутствие утверждения о не-первом чанке,
|
||||
отсутствие записей каталога по шву; Д1 — новая ja-ru пара с чужими словами (зелено), переименование в zh-ru
|
||||
(один красный, штатный рефреш, зелено), 0 тестов на `terminology.Types`/`bankTypeOK`, содержимое
|
||||
`prompt_lint_test.go`; Д2 — проба 1 (один сабтест), проба 2 (зелено при врущем зеркале), 6 коммитов файла с
|
||||
байт-идентичным блоком армов, YAML-контролы, строка 114 с 02.08. Ни одно из заказанных воспроизведений не
|
||||
опровергло утверждение; расхождения — только адреса на 1–2 строки, поправлены в тексте.
|
||||
|
||||
**Взято с агентов замера, не сверял:** сквозной прогон Д1 с фейк-провайдером; `git log -L` по телам правил
|
||||
заголовка; 300/300 многочанковых глав; шифт-мап и компилятор-пробы §7; всё в §7.1; «1079 строк» проверил, «16
|
||||
тестов» — нет; «5 боевых call-site `TargetChecksFor`» — по моему грепу их 3 + определение (в `chunk`/`text`
|
||||
ноль в любом счёте).
|
||||
|
||||
**Чему доверяю меньше всего:**
|
||||
1. Утверждению, что расщепление `EmbeddedVersion` — «24 строки + три call-site» и что образец `bankdata.go`
|
||||
переносится один-в-один: три радиуса требуют решить, к какому относится каждый из 8 файлов (`injection.txt`
|
||||
едет на провод, `cjk-section.txt`/`terminator.txt` — резка, `target-ru.txt` — вердикт, `refusal.txt`,
|
||||
`sentence-abbrev.txt`, `lang-script.txt`, `script-series.txt` — не классифицированы ни одним агентом). Это
|
||||
дизайн-вопрос, а смета — оценка по чтению.
|
||||
2. Моей же поправке к Д5 (вердикт-ось требует переклассификации): `resume.go:24-26` сверено, но какие именно
|
||||
стадийные вердикты зависят от каких версий (`sanitizer` — да; `coverage`/`style_check` — по комментариям
|
||||
«observability, never hard gates», то есть, возможно, ни от чего) — по коду не размечал. Направление
|
||||
уверенное, объём — нет.
|
||||
3. Частотному доводу Д3 из `git log -L` («тела правил заголовка двигались по 3 раза каждое»): переезд файлов
|
||||
из `pipeline` в `chunk` (`e33e3be`) ломает `-L`, и агент замера это не оговорил; мой счёт по файлам
|
||||
(9/6/4 коммита) — другая популяция. (Историю армов `ThinksOnWire` мой агент снял по всем 6 ревизиям файла
|
||||
целиком — ей доверяю.)
|
||||
4. Числу «~$9–11 на 500 глав» — это оценка самого проекта (строка 185), не замер этого воркфлоу.
|
||||
|
|
@ -348,6 +348,13 @@ LITERALS = [
|
|||
# что и правка регистра, и «переписывать историю» этот гард ни от кого не требует.
|
||||
(PROGRESS, "current-state", r"открытых рядов регистра платформы — (\d+)", ("регистр-статус", "open")),
|
||||
(PROGRESS, "current-state", r"открытых рядов регистра платформы — \d+ \(major (\d+)\)", ("регистр-вес", "major")),
|
||||
# ⚠ ИТОГ РЯДОВ ДОБАВЛЕН 06.09, И ДЫРА БЫЛА ИМЕННО ТОЙ ФОРМЫ, ПРОТИВ КОТОРОЙ СТОИТ ВЕСЬ ЭТОТ СПИСОК:
|
||||
# строка CURRENT-STATE говорила «всего рядов 453 (пере-счёт `counts.py --check`)» при РЕАЛЬНЫХ 457, и
|
||||
# `--check` печатал «литералы сходятся» — потому что итог не сверялся НИКОГДА, а проза утверждала, что
|
||||
# сверяется. Открытые и major сторожились, итог ехал рядом с ними и читался как такой же охраняемый.
|
||||
# Нашла внешняя ревизия, не гейт: у гейта не было на это вопроса. Цена такой дыры выше обычной ошибки —
|
||||
# число под ложным обещанием проверки доверенней непроверенного.
|
||||
(PROGRESS, "current-state", r"всего рядов (\d+)", ("регистр-итог", "")),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -733,6 +740,8 @@ def main() -> int:
|
|||
want = b[key]
|
||||
elif kind == "регистр-статус":
|
||||
want = r["по статусу"].get(key, 0)
|
||||
elif kind == "регистр-итог":
|
||||
want = r["всего строк"]
|
||||
else:
|
||||
want = r["вес открытых"].get(key, 0)
|
||||
found = re.search(pattern, region(texts[path], reg_name))
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@
|
|||
| В-8 | **Какое слово о состоянии книги стоит в дереве.** Замечание 23 потребовало бейджу ФИКС-место, и оно сделано; но две метки словаря в него не помещаются и усекаются с тултипом — «остановлена: лимиты» (19 знаков) и ветка неизвестного «неизвестное состояние» (21). Укоротить их сессия НЕ вправе: фраза паузы зарезервирована за владельцем (прежний В-6 снят с листа 02.09 как ЗАКРЫТЫЙ; носитель — `docs/architecture/14-api-contract/README.md:1054`=`не различает две беды`, закрыт D39.132 п.2а), и первый заход S3.5 её укоротил и был откачен собственным ревью. Варианты: (а) оставить усечение с тултипом, (б) владелец даёт короткое слово для дерева, а полная фраза остаётся в статус-полосе, (в) расширить колонку бейджа за счёт названия книги. ⚠ Сверить с D39.176 п.4 (30.08): фразы, которые видит пользователь, ДЕЛЕГИРОВАНЫ проекту — вопрос к владельцу может сузиться до раскладки (а)/(в) | продуктовая формулировка, которую видит пользователь |
|
||||
| В-7 | **Физический масштаб интерфейса на мониторе.** Замечание 1 говорит «масштабы великоваты, особенно на 27″ 2K, на 24″ терпимее». Замер при вьюпорте референса расхождения с Fleet не нашёл (§5.5 плана): промежутки, ширины панелей, шаг строки и высота вкладки совпадают. Значит речь про физический размер на конкретном экране, а он зависит от масштабирования ОС, и «просто уменьшить токены» означало бы уехать от референса, который владелец принял. Правильное лекарство — ручка «плотность интерфейса» в настройках (3 ступени), но она требует перевода ВСЕХ размерных токенов в rem и пере-замера, то есть отдельного прохода (Ф-36). Вопрос: нужна ли ручка, и какой масштаб ОС стоит на 27″ 2K — по нему видно, во сколько раз расходится физика | продуктовое решение и цена отдельного пакета |
|
||||
| В-9 | **Какие языки интерфейса продукт обещает.** Механизм переводимости построен (S3.7; устройство и границы — `STACK_DECISIONS.md` §1, числа — строка Ф-35). Чего фронт сделать не может и не должен: решить, какие языки продукт обещает, и выдумать сами переводы. Пока список из одного значения (`ru`), и каркасный пункт настроек честно говорит «сейчас интерфейс только русский» | продуктовое решение и обещание пользователю |
|
||||
| В-11 | **Фразы отказа книги (PD-173) — на утверждение.** Машинную причину несёт контракт, слова пишет клиент. ⚠ **Словарь с тех пор сменился, фразы ниже собраны на 0.2.3 и при разморозке пере-собираются:** канон даёт ЧЕТЫРЕ причины — `parser_unavailable` переименован в `processing_failed`, добавлен `content_refused` (`docs/architecture/14-api-contract/openapi.yaml:1558`=`enum: [source_unreadable, not_configured, processing_failed, content_refused]`), а карта «код → фраза» живёт приложением А компаньона. Фразы сочинила сессия S4 по слову владельца 14.08 и они ждут его «да»: «Файл не удалось разобрать на разделы» / «Проверьте файл — возможно, это не текст книги — и добавьте её заново»; «Сервис пока не готов принять эту книгу» / «Файл здесь ни при чём: повторная загрузка поможет не раньше, чем мы это наладим»; «Не удалось обработать файл на нашей стороне» / «Файл здесь ни при чём: попробуйте добавить книгу позже»; неизвестная причина — «Книгу не удалось принять» / «Причина не названа. Попробуйте добавить книгу заново или позже» | продуктовая формулировка, которую видит пользователь |
|
||||
| В-11 | **Фразы отказа книги (PD-173) — на утверждение.** Машинную причину несёт контракт, слова пишет клиент. ⚠ **Словарь с тех пор сменился, фразы ниже собраны на 0.2.3 и при разморозке пере-собираются:** канон даёт ЧЕТЫРЕ причины — `parser_unavailable` переименован в `processing_failed`, добавлен `content_refused` (`docs/architecture/14-api-contract/openapi.yaml:1606`=`enum: [source_unreadable, not_configured, processing_failed, content_refused]`), а карта «код → фраза» живёт приложением А компаньона. Фразы сочинила сессия S4 по слову владельца 14.08 и они ждут его «да»: «Файл не удалось разобрать на разделы» / «Проверьте файл — возможно, это не текст книги — и добавьте её заново»; «Сервис пока не готов принять эту книгу» / «Файл здесь ни при чём: повторная загрузка поможет не раньше, чем мы это наладим»; «Не удалось обработать файл на нашей стороне» / «Файл здесь ни при чём: попробуйте добавить книгу позже»; неизвестная причина — «Книгу не удалось принять» / «Причина не названа. Попробуйте добавить книгу заново или позже» | продуктовая формулировка, которую видит пользователь |
|
||||
| В-2 | **Авторское право на текст фикстур.** Витрина использует короткую выдержку из настоящего прогона (蛊真人). Правило проекта уже есть: `eval/.gitignore:4` держит копирайтные тексты вне git. Сейчас взят объём под проверку плотности и не больше; нужно ли убрать совсем и заменить синтетикой — решать владельцу | юридика |
|
||||
| В-4 | **Чем подписывать главу в дереве, если заголовок — ровно «Глава N».** Титул движок рендерит детерминистически из шаблона пары (`configs/langpacks/zh-ru/heading.txt`: `template Глава {n}`), а подзаголовок остаётся телом текста и названием НЕ является. На 2284 разделах это дерево из 2284 одинаковых по форме строк. Оставить так, показывать номер иначе или просить у контракта что-то ещё — продуктовое решение. Артефакт §1.4 и К-3 | продуктовая формулировка, которую видит пользователь |
|
||||
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
|
||||
**Фикс-лист ФС (носитель состава — эта запись; вход промта S5):** ФС-1 (major, мир intake) прогон над загруженной книгой стартует с `begin(0)` — общий прогресс 0/0 весь прогон, ETA = NaN (лечение: begin суммой units_total глав); путь заказа §4 «книга из формы переводится» в мок-мире добить · ФС-2 showcase: принятый старт прогона не меняет мир — следующее чтение отдаёт прежнее состояние, «Перевести» выглядит как ничего · ФС-3 мир intake: повторный старт переиспользует id прогона — поток не переподписывается, translating вечно (verify_bank тем же кластером: наблюдаемого эффекта в моках нет, awaiting_bank недостижим) · ФС-4 `BookIntake.title` maxLength 200 не держит никто на клиенте (TextField без лимита — собственная спека нарушаема своим клиентом) · ФС-5 докблок `upload.ts` несёт СНЯТОЕ правило «field after file → 400» (неверно для необязательного) · ФС-6 ИСПОЛНЕНО 02.09: порядковый номер шага spectral убран из обоих доков (греп `шестым шагом` по зоне пуст; состав шагов сверяется по `package.json`, `STACK_DECISIONS.md` §3) · ФС-7 мок считает части формы по ВСЕЙ форме (платформа — до файла) и меряет 413 по file.size (платформа — по всему телу) — мок строже провода · ФС-8 флейк сцены refusals: closeModal не дождался закрытия за 5000 мс (1 из 3 на тихой машине) · ФС-9 `role="status"` оборачивает байтовый прогресс отправки — на живом XHR скринридер зачитывает каждое обновление (в регионе — только смены состояний) · ФС-10 группа из одного раздела подписывается «разделы 201–201» · ФС-11 интейк-мир: ПЕРВОЕ нажатие «Перевести» всегда 409 (счётчик; кандидат — маркер) · ФС-12 после-пакные числа Ф-35 слегка отстают от дерева (пересборка: 800293 / 246670 gzip против записанных 800219 / 243446).
|
||||
|
||||
**Поправки к записи S4** (сама запись — в [archive/frontend-PROGRESS-S0-S4.md](archive/frontend-PROGRESS-S0-S4.md); чужой текст не редактирую): «словарь платформы — четыре значения» → в коде ПЯТЬ (пропущен `schema_mismatch`) · «одна из трёх фраз сегодня недостижима» → на 15.08 недостижимы были ВСЕ ТРИ: платформа `reject_reason` не проецировала, доезжала только ветка неизвестного. ⚠ Пере-сверено 02.09: с тех пор проекция ПОСТРОЕНА — пять внутренних причин сводятся к трём проводным (`platform/internal/ingest/vocabulary.go:59`=`func ContractRejectReason`), то есть фразы В-11 стали достижимы и поправка касается только архивной записи · §1/§2 записи несут снятое правило PD-172 «поле после файла = 400», которое сама же §9а опровергла (код и спека исправлены, текст записи — нет) · «136 проверок» = 134 (наивный греп ловил 2 строки vite) · «31 строка DOM / 59 618px» — замер S2 04.08, не этого пака · «46 находок → 6 выжило» = 5 различных (одна — дубль) · три поправки к записи S3.7 из заказа §6: разобрана только цена бандла; «дрейф ширин hidden» и «счёт 106 из 119» остались на записи оркестратора 14.08 (носитель там, дефекта сессии нет — фиксирую для полноты).
|
||||
**Поправки к записи S4** (сама запись — в [archive/frontend-PROGRESS-S0-S4.md](archive/frontend-PROGRESS-S0-S4.md); чужой текст не редактирую): «словарь платформы — четыре значения» → в коде ПЯТЬ (пропущен `schema_mismatch`) · «одна из трёх фраз сегодня недостижима» → на 15.08 недостижимы были ВСЕ ТРИ: платформа `reject_reason` не проецировала, доезжала только ветка неизвестного. ⚠ Пере-сверено 02.09: с тех пор проекция ПОСТРОЕНА — пять внутренних причин сводятся к трём проводным (`platform/internal/ingest/vocabulary.go:72`=`func ContractRejectReason`), то есть фразы В-11 стали достижимы и поправка касается только архивной записи · §1/§2 записи несут снятое правило PD-172 «поле после файла = 400», которое сама же §9а опровергла (код и спека исправлены, текст записи — нет) · «136 проверок» = 134 (наивный греп ловил 2 строки vite) · «31 строка DOM / 59 618px» — замер S2 04.08, не этого пака · «46 находок → 6 выжило» = 5 различных (одна — дубль) · три поправки к записи S3.7 из заказа §6: разобрана только цена бандла; «дрейф ширин hidden» и «счёт 106 из 119» остались на записи оркестратора 14.08 (носитель там, дефекта сессии нет — фиксирую для полноты).
|
||||
|
||||
*(Запись приёмки S4 с фикс-листом ФС — ВЫШЕ, она живая; всё остальное по S0–S4 вынесено в архив, см. шапку.)*
|
||||
|
||||
|
|
|
|||
2
platform/.gitignore
vendored
2
platform/.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
|||
# Рабочий артефакт `make check`: рецепт пишет его в корень зоны и сам удаляет,
|
||||
# но прерванный прогон оставляет — а коммитит зону оркестратор.
|
||||
.check.log
|
||||
.check.log*
|
||||
|
|
|
|||
|
|
@ -117,23 +117,60 @@ conditions:
|
|||
# It also prints the register gate's findings (internal/gates, `ALARM` lines): open rows whose weight
|
||||
# says minor and whose words say money, silence or a hold — the class a reconnaissance that counts
|
||||
# only `major` never sees.
|
||||
#
|
||||
# ⛔ THE LOG IS PER-RUN AND ITS ABSENCE IS RED, and both halves were bought by a defect. Every line
|
||||
# this recipe prints is a GREP over that file, and grep answers a missing file exactly as it answers a
|
||||
# clean one: with nothing. So the recipe used to reach its `else` and print «every test ran: no host
|
||||
# condition was missing» over a run whose log had vanished — a bill of health for a measurement that
|
||||
# never happened. Observed 06.09: three `grep: .check.log: No such file or directory` followed by that
|
||||
# very line, on a run with FIVE skips.
|
||||
#
|
||||
# The cause was the FIXED name: two batteries in one directory — normal here, the zone and the
|
||||
# orchestrator both run it — and whichever finished first deleted the other's evidence mid-recipe.
|
||||
# `$$$$` is the shell's PID, so runs no longer share a file. The guard below is the second half and
|
||||
# the one that matters: it covers every OTHER way a log can go missing (a failed redirect, a full
|
||||
# disk, a hand), and it turns «nothing to read» into a failure instead of a clean bill. ⚠ The two are
|
||||
# not redundant — the unique name removes today's cause, the guard removes the CLASS.
|
||||
#
|
||||
# ⛔ AND THE SWEEP IS THE PRICE OF THE PER-RUN NAME, not tidiness. The fixed name limited itself: a
|
||||
# failed run left ONE stale file and the next run wrote over it. Per-run names removed that property
|
||||
# along with the collision, so every failed or interrupted run leaves its log behind for good — four
|
||||
# of them accumulated in one night. They are gitignored, so nothing reaches a commit; what rots is
|
||||
# READABILITY, and precisely where it is load-bearing: this recipe KEEPS the log on failure on
|
||||
# purpose, and a heap of debris from interrupted runs is indistinguishable from the one file somebody
|
||||
# was told to read.
|
||||
#
|
||||
# ⚠ IT SWEEPS ONLY LOGS WHOSE WRITER IS GONE (`kill -0`, which is POSIX and needs no `/proc`), and
|
||||
# that condition is the whole of it: a bare `rm -f .check.log.*` would delete a CONCURRENT run's log
|
||||
# and restore the very defect the per-run name was introduced to remove. A recycled PID keeps a stale
|
||||
# file — the conservative direction, and the only one that cannot destroy evidence. ⚠ Consequence
|
||||
# worth knowing rather than discovering: the log kept by a FAILED run is swept by the NEXT run, so
|
||||
# read it before re-running.
|
||||
check: build vet fmt lint sqlc-check
|
||||
@$(GO) test ./... -race -count=1 -v > .check.log 2>&1; status=$$?; \
|
||||
grep -E '^(ok|FAIL|\?)' .check.log || true; \
|
||||
if grep -q 'ALARM PD-' .check.log; then \
|
||||
echo "--- open register rows below major that carry alarm markers (internal/gates) ---"; \
|
||||
grep -o 'ALARM PD-.*' .check.log; fi; \
|
||||
if [ $$status -ne 0 ]; then \
|
||||
echo "--- FAILURES ---"; grep -E '^(---|[[:space:]]+---) FAIL' .check.log; \
|
||||
echo "--- the log is kept at .check.log: the message under a failing test is where it says what to do ---"; \
|
||||
@for f in .check.log.*; do [ -e "$$f" ] || continue; p=$${f##*.}; \
|
||||
case "$$p" in ''|*[!0-9]*) continue;; esac; \
|
||||
kill -0 "$$p" 2>/dev/null || rm -f "$$f"; done; \
|
||||
log=.check.log.$$$$; \
|
||||
$(GO) test ./... -race -count=1 -v > "$$log" 2>&1; status=$$?; \
|
||||
if [ ! -s "$$log" ]; then \
|
||||
echo "--- THE BATTERY LEFT NO LOG ($$log): every line below would be a grep over a file that is not there ---"; \
|
||||
echo "--- and a missing file is INDISTINGUISHABLE from a clean one to grep, so this exits RED rather than printing a bill of health nobody measured ---"; \
|
||||
exit 1; fi; \
|
||||
if grep -q -- '--- SKIP' .check.log; then \
|
||||
echo "--- did NOT run: $$(grep -c -- '--- SKIP' .check.log) skipped. Host conditions the battery reads (from the test sources), each with what it opens: ---"; \
|
||||
grep -E '^(ok|FAIL|\?)' "$$log" || true; \
|
||||
if grep -q 'ALARM PD-' "$$log"; then \
|
||||
echo "--- open register rows below major that carry alarm markers (internal/gates) ---"; \
|
||||
grep -o 'ALARM PD-.*' "$$log"; fi; \
|
||||
if [ $$status -ne 0 ]; then \
|
||||
echo "--- FAILURES ---"; grep -E '^(---|[[:space:]]+---) FAIL' "$$log"; \
|
||||
echo "--- the log is kept at $$log: the message under a failing test is where it says what to do ---"; \
|
||||
exit 1; fi; \
|
||||
if grep -q -- '--- SKIP' "$$log"; then \
|
||||
echo "--- did NOT run: $$(grep -c -- '--- SKIP' "$$log") skipped. Host conditions the battery reads (from the test sources), each with what it opens: ---"; \
|
||||
$(MAKE) --no-print-directory -s conditions; \
|
||||
echo "--- skipped tests ---"; \
|
||||
grep -- '--- SKIP' .check.log; \
|
||||
grep -- '--- SKIP' "$$log"; \
|
||||
else echo "--- every test ran: no host condition was missing ---"; fi; \
|
||||
rm -f .check.log
|
||||
rm -f "$$log"
|
||||
|
||||
# Not in `check`: fuzzing is time-boxed exploration, not a gate. The seed corpus runs as an
|
||||
# ordinary test on every `check`; this target is for going deeper on the decoder.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -7,6 +7,32 @@
|
|||
> Общая записка по обоим новым сервисам — `frontend/docs/STACK_DECISIONS.md` §5 (02.08); здесь —
|
||||
> платформенная часть с датами релизов и сверкой.
|
||||
|
||||
## Указатель решений по ПРЕДМЕТУ (не по сессии)
|
||||
|
||||
> ⚠ **Секции «Что решено сессией P…» — это НЕ хроника, а РЕЕСТР:** решения нумерованы сплошь **1–36**
|
||||
> без пропусков, и группировка по сессиям — лишь атрибуция (кто и когда принял). Указатель добавлен
|
||||
> 06.09, потому что ось «по сессии» заставляла искать правило перебором семи секций; **резать эти
|
||||
> секции НЕЛЬЗЯ — там живой свод правил зоны.**
|
||||
|
||||
| Предмет | Решения |
|
||||
|---|---|
|
||||
| HTTP-поверхность, маршруты, заголовки | 1 · 2 · 3 · 4 · 5 |
|
||||
| Батарея и тесты с БД | 6 · 7 |
|
||||
| Миграции и схема (append-only) | 8 |
|
||||
| Личность, сессии, вход, дев-двери | 9 · 13 · 14 · 30 · 31 |
|
||||
| Админ-поверхность и секреты | 10 · 11 |
|
||||
| Таймауты сервера и дедлайны чтения | 12 · 25 |
|
||||
| Раннер: systemd, срез, маркер конца, трассировка | 15 · 16 · 17 · 18 · 32 |
|
||||
| Очередь задач | 19 |
|
||||
| Деньги и потолки | ⛔ **20 — ОТМЕНЕНО** паком «форма заказа» 05.09 · 21 |
|
||||
| Порядок блокировок в сторе | 22 |
|
||||
| Материализация read-модели и её долг | 23 · 33 · 34 |
|
||||
| Метрики, наблюдаемость, атомарность | 24 · 36 |
|
||||
| Приём книги и различение стопа | 26 · 27 |
|
||||
| Зависимости | 28 |
|
||||
| Шаблоны и рендер | 29 |
|
||||
| Свойства книги (редактор пайплайна) | 35 |
|
||||
|
||||
## Пины
|
||||
|
||||
| Что | Пин | Релиз пина | Зачем нам |
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
# Журнал зоны «Платформа»
|
||||
|
||||
## ПАК «ФОРМА ЗАКАЗА ПЕРЕВОДА» — ОТЧЁТ (05.09, `textmachine-main-34`)
|
||||
## ПАК «ФОРМА ЗАКАЗА ПЕРЕВОДА» — ОТЧЁТ (05–06.09, `textmachine-main-34`)
|
||||
|
||||
> Пак `docs/ORDER_FORM_SESSION_PROMPT.md`. Дерево передано оркестратору `textmachine-main-27` НЕ
|
||||
> закоммиченным; зона не коммитит. Записка-план стояла на этом месте и заменена отчётом.
|
||||
> ⚠ **Батарея сдаётся с ОДНИМ названным красным** — см. «Батарея», он ратифицирован и закрывается
|
||||
> вторым актом оркестратора.
|
||||
>
|
||||
> ⛔ **ПАК БЫЛ ЗАЛЕНДЕН 06.09 (`628cc56`, `36ea8b8`, акт `D39.208`), И ПОСЛЕ ЛЕНДИНГА ОТКРЫТ СНОВА.**
|
||||
> Владелец заказал зоне проконтролировать, что оркестратор действительно закрыл переданные ему
|
||||
> находки. Контроль нашёл ЧЕТЫРЕ вещи, ТРИ из которых — мои, и одна из них уже ехала ложью на проводе
|
||||
> в ратифицированном каноне. Разбор — секция «**ПЯТЫЙ ЗАХОД**»; следствия — новый минор **0.12.0**,
|
||||
> новая форма `Run` (`ordered_units`), закрытый шов материализатора и перевёрнутый пин.
|
||||
> ⚠ Числа батареи, стоящие в секции «Батарея» ВЫШЕ блока «ФИНАЛ 06.09», — прежние и оставлены
|
||||
> историей захода; действующие ниже.
|
||||
|
||||
### ЧТО ИЗМЕНИЛОСЬ ПО СУЩЕСТВУ, одним абзацем
|
||||
|
||||
|
|
@ -36,9 +44,19 @@
|
|||
волна по всей книге больше не фанится: `volumeScope.allows` гейтит и draft-волну (`leader`-карта
|
||||
«flattened for the draft wave», `backend/internal/pipeline/volume.go`). ⇒ **посылка стала истинной, а
|
||||
формула — нужной:** непочерновленный остаток заказа это реальная работа прогона, и удаление члена
|
||||
сделало бы знаменатель полосы неверным по-новому. Снята ПРИЧИНА дефекта, носитель оставлен. Полоса
|
||||
при этом осталась в ГЛАВАХ — канон §Progress говорит «in chapters», перевод её в юниты это отдельная
|
||||
контрактная семантика, которой в составе минора (строка 279) нет.
|
||||
сделало бы знаменатель полосы неверным по-новому. Снята ПРИЧИНА дефекта, носитель оставлен.
|
||||
|
||||
⛔ **ИСПРАВЛЕНО ПЯТЫМ ЗАХОДОМ 06.09, И ЭТО БЫЛА МОЯ ЛОЖЬ О СВОЁМ ЖЕ КОДЕ.** Здесь стояло «полоса при
|
||||
этом осталась в ГЛАВАХ ⇒ перевод её в юниты это отдельная семантика, которой в составе минора нет».
|
||||
**Неверно.** `runDone`/`runTotal` (`internal/pgstore/readmodel.go:632,636`=`caption stuck on drafting`) первой же ветвью — `when
|
||||
r.ordered_units is not null then` — считают полосу в ЮНИТАХ, и это намеренно и пиньнуто
|
||||
(`runs.TestACharacterOrdersBarIsCountedInWhatItActuallyBought`, страж обратной стороны —
|
||||
`TestAChapterOrderKeepsTheBarItAlwaysHad`). Абзац остался от редакции ДО того, как юнитовая ветка была
|
||||
добавлена, и не был пере-читан. **Цена лжи не нулевая:** на этой фразе состав минора был сдан без
|
||||
третьего случая `Progress`, канон 0.11.0 залендился (`docs/architecture/14-api-contract/openapi.yaml`
|
||||
§`Progress`, строки 1449, 1479, 1483) со словами «in chapters» / «Chapters of this run's own work», —
|
||||
и сборка сегодня шлёт по этому полю ЮНИТЫ. Это **ложь на проводе**, заведённая не кодом, а отчётом.
|
||||
Позиция внесена в состав минора ниже; оркестратору написано отдельно.
|
||||
|
||||
### РЕШЕНИЯ, КОТОРЫЕ ПРОМТ ОСТАВИЛ ЗОНЕ, — с доводами
|
||||
|
||||
|
|
@ -143,6 +161,324 @@
|
|||
«ожидаемо $0.00, зарезервируем $2.07»; арифметика холда не была ограничена сверху и на абсурдной
|
||||
проекции переполняла int64 в правдоподобное МАЛОЕ число.
|
||||
|
||||
### ⛔ ЧЕТВЁРТЫЙ ЗАХОД — ПРОВЕРКА СОБСТВЕННЫХ УТВЕРЖДЕНИЙ ОТЧЁТА, а не кода
|
||||
|
||||
Заход сделан по прямому вопросу владельца «нет ли чего, что ты умолчала или поленилась допроверить».
|
||||
Проверялись не находки ревьюеров, а МОИ СОБСТВЕННЫЕ фразы в этом отчёте — те, где стоит утверждение,
|
||||
но не стоит команда.
|
||||
|
||||
1. ⛔ **Объяснение чистого `sqlc diff` было ДОГАДКОЙ и оказалось неверным** — см. секцию «Батарея».
|
||||
Вывод («пере-генерация не нужна») верен, причина — нет, и ложная причина скрывала живой риск.
|
||||
2. ⛔ **`Down`-путь миграции НЕ был пере-проверен** после двух последних правок схемы (колонки
|
||||
юнитовой полосы добавлены, колонки цены главы сняты). Пере-снято на отдельной базе: `up` даёт 4/4
|
||||
новых колонки `runs` и 0 колонок цены у `chapters`, `down` и повторный `up` чисты.
|
||||
3. ⛔ **`source_chars` писался ТОЛЬКО когда книга оценена** — то есть манифест, честно назвавший
|
||||
текст и споткнувшийся на ДЕНЕЖНОМ ключе, лишал экран честного числа знаков, и «Знаков» падало
|
||||
обратно на приближение интейка (для EPUB — руны ZIP-архива). Два независимых факта были связаны
|
||||
одним условием. Развязано; пин `TestTheCharacterCountSurvivesAPriceThisBuildCouldNotRead`.
|
||||
⚠ Комментарий миграции при этом обещал «null пока манифест не прочитан» — то есть код и
|
||||
комментарий расходились, и заметил это не гейт.
|
||||
4. ⛔ **Починка «доставка ЗА границей заказа не гасит работу внутри неё» была без пина** — я объявила
|
||||
её сделанной, имея только код. Заведён `TestDeliveryBeyondTheOrderDoesNotCancelWorkOwedInsideIt`.
|
||||
5. ⛔ **Таблица named-артефакта несла ВОСЕМЬ строк, а посадок девять** — девятая была описана прозой
|
||||
в соседней секции. Читающий артефакт по его собственной таблице видел восемь. Строка добавлена.
|
||||
|
||||
⚠ **Что этот заход говорит о предыдущих:** четыре из пяти — не дефекты кода, а дефекты МОИХ
|
||||
УТВЕРЖДЕНИЙ О КОДЕ, и ни один гейт их поймать не мог по построению. Это тот же класс, который
|
||||
`D39.202` п.9 называет главным: сторожат поведение и форму, а не то, что код и отчёт о себе ГОВОРЯТ.
|
||||
|
||||
### ⛔ ПЯТЫЙ ЗАХОД — 06.09, ПОСЛЕ ЛЕНДИНГА, ПО ЗАКАЗУ ВЛАДЕЛЬЦА «проконтролируй, что оркестратор их закрыл»
|
||||
|
||||
Заход начинался как КОНТРОЛЬ ЗАКРЫТИЯ чужой рукой и нашёл четыре вещи, три из которых мои.
|
||||
|
||||
1. ⛔⛔ **ЛОЖЬ НА ПРОВОДЕ В УЖЕ ЗАЛЕНДЁННОМ КАНОНЕ 0.11.0, И ЗАВЕЛ ЕЁ ЭТОТ ОТЧЁТ.** Здесь стояло
|
||||
«полоса осталась в ГЛАВАХ ⇒ перевод её в юниты это семантика, которой в составе минора нет».
|
||||
Фраза ложна: `runDone`/`runTotal` ПЕРВОЙ ветвью считают в юнитах (`readmodel.go:632,636`), и это
|
||||
пиньнуто (`TestACharacterOrdersBarIsCountedInWhatItActuallyBought`). Абзац остался от редакции ДО
|
||||
юнитовой полосы — той самой, про которую я же написала «добавлена последней и проверена тоньше
|
||||
остального». **Не пере-читала.** На этой фразе состав минора уехал без третьего случая `Progress`,
|
||||
и канон залендился со словами «in chapters» там, где сборка шлёт юниты. Исправлено в составе,
|
||||
написано оркестратору. ⚠ Это ровно тот отказ, который промт называет «связность вместо истинности»:
|
||||
абзац был СВЯЗЕН с прежней редакцией пака и потому читался верным.
|
||||
2. ⛔ **МОЯ ЖЕ ПОЧИНКА ИЗ ЧЕТВЁРТОГО ЗАХОДА (п.3) ЗАВЕЛА ВТОРОЙ НОСИТЕЛЬ ОДНОГО ЧИСЛА** — и уронила
|
||||
соседний тест, `TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy`
|
||||
(«the engine's rune count did not land: <nil>»). Развязав `source_chars` от цены, я добавила
|
||||
`pgstore.Structure.SourceChars` РЯДОМ с `Structure.Price.SourceChars` (тип `Price` был проводной
|
||||
`ingest.BookPrice`) — то есть одно число в двух полях одного значения, и заполнить можно было
|
||||
любое. **Починено структурно, а не подгонкой фикстуры:** у `pgstore` теперь свой тип `Projection`
|
||||
с ТРЕМЯ денежными членами и БЕЗ счёта знаков, так что второе поле физически негде заполнить.
|
||||
Радиус правки — три места. ⚠ Урок против моего же четвёртого захода: развязывание двух фактов
|
||||
доделано наполовину — я добавила носитель, но не сняла старый, и получила ту самую энтропию,
|
||||
про которую фикстура `priced()` двумя строками ниже сама предупреждает («a second carrier free to
|
||||
drift»).
|
||||
3. ⛔ **ТРИ КОММЕНТАРИЯ НАЗЫВАЛИ ИМЯ, КОТОРОГО НЕ СУЩЕСТВУЕТ:** `DeliveredUnits` / поле провода
|
||||
`delivered_units` (`pgstore/readmodel.go:650`, `pgstore/books.go:819`, `runs/order_test.go:95` —
|
||||
там настоящее имя `SpawnOrder.Delivered`). Ни в Go, ни на проводе, ни в каноне такого нет. Это
|
||||
класс строки бэклога **299**, и второй из трёх ВРАЛ ПРО КОНТРАКТ, обещая читателю несуществующее
|
||||
поле рядом с `delivered_chapters`. Переписаны на правду. ⚠ Замечание к самой 299, отдано
|
||||
оркестратору: греп по `Test…` этого класса не ловит — имена были полевые и wire-овые.
|
||||
4. ⛔ **ТРИ ФАЙЛА ЭТОГО ПАКА НЕ ЗАЛЕНДИЛИСЬ** и лежали в дереве как ` M`: `pgstore/readmodel.go` ·
|
||||
`readmodel/readmodel.go` · `pgstore/price_test.go` — вся починка п.3 четвёртого захода вместе с её
|
||||
пином. Сказано оркестратору, он забрал их коммитом `32bbadb` в 02:06.
|
||||
5. ⛔⛔ **И Я ПОПРОСИЛА ЕГО ЗАБРАТЬ ТО, ЧТО НИКОГДА НЕ БЫЛО ЗЕЛЁНЫМ.** Прогон, шедший в тот момент,
|
||||
уже держал в логе красноту п.2 — я написала письмо раньше, чем прочла его лог. `32bbadb` уехал в
|
||||
историю с падающим тестом, и снял это только следующий коммит. **Это ВТОРОЙ раз за пак:** четвёртый
|
||||
заход поймал ровно тот же отказ (п.4 там — «объявила починку сделанной, имея только код»). Разница
|
||||
в том, что теперь цена вышла за пределы зоны — чужой рукой в общую историю. ⇒ норма, записанная
|
||||
себе и следующей смене: **ничего не отдавать на лендинг без прогона ПАКЕТА, которого правка
|
||||
касалась**; «код написан и пин заведён» — не то же, что «прогнано».
|
||||
|
||||
6. ⛔ **ВТОРАЯ ЛОЖЬ В ТОМ ЖЕ ЗАЛЕНДЁННОМ КАНОНЕ, и на этот раз ЕГО текст:** `Run.ordered_chapters`
|
||||
обещает `0` для символьного заказа. ИЗМЕРЕНО — приезжает СПАН, ≥1 и завышающий (пять юнитов из
|
||||
двенадцати читаются как «2»). Подробности и предложенная формулировка — в составе минора выше.
|
||||
⚠ Мой первый различитель, отданный оркестратору письмом, опирался на этот ноль и был НЕВЕРЕН.
|
||||
Поправка послана. **Настоящий различитель ОДИН: `delivered_chapters`.**
|
||||
7. ⛔ **«UNIT-SHAPED» БЫЛО ОПРЕДЕЛЕНО НЕ ТАК, КАК РАБОТАЕТ, В ПЯТИ МЕСТАХ.** Везде стояло «the order
|
||||
does not close whole chapters», а `q.UnitShaped = true` ставится БЕЗУСЛОВНО для каждого заказа,
|
||||
выраженного знаками. `UnitsFor` возвращает первый юнит, чья нарастающая сумма покрывает
|
||||
запрошенное ⇒ заказ садится ровно на границу главы примерно так же часто, как главы делят юниты:
|
||||
**при трёх юнитах на главу — каждый третий символьный заказ.** Такой заказ закрывает целые главы и
|
||||
всё равно получает юнитовую полосу и `delivered_chapters: null`. Четыре носителя мои — исправлены
|
||||
(`pricing.Quote.UnitShaped`, `runs.go` у `OrderedUnits` и у спана, `unitShapedOrder`,
|
||||
`httpapi.wireRun.DeliveredChapters`); пятый — канон, отдан оркестратору. **Поведение зона считает
|
||||
верным и НЕ меняла:** две покупки, которые человек не отличает друг от друга, не должны рисоваться
|
||||
по-разному, а при определении «по факту попадания» одинаковые на вид заказы получали бы то юнитовую
|
||||
полосу, то главную. Ложны были ОПРЕДЕЛЕНИЯ. Пин —
|
||||
`TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`, он же ловит и находку 6. ⚠ Пин заведён
|
||||
под прежним именем (`…ReportsTheChapterSpan…`) и ПЕРЕИМЕНОВАН вместе с переворотом — см. п.0
|
||||
заявления `D39.183`.
|
||||
|
||||
### ⛔ ДОФИКС ПО КАНОНУ 06.09 — ДВА УТВЕРЖДЕНИЯ О ЗНАЧЕНИЯХ, ПЕРЕПИСАННЫЕ ЗОНОЙ ИЗ СВОИХ ПИНОВ
|
||||
|
||||
Заказ оркестратора по норме авторства (строка бэклога 309 / `D39.208`): **утверждение канона о
|
||||
ЗНАЧЕНИИ пишет ЗОНА из своих пинов и называет пин.** Оба утверждения были написаны им без прогона и
|
||||
оказались ложными. Правился ТОЛЬКО текст и схема канона; поведение не менялось ни в одном месте.
|
||||
|
||||
1. **`Run.ordered_chapters` нёс `minimum: 1` при собственной прозе «`0` is legal and means a
|
||||
re-pass».** Схема противоречила своему же описанию, и валидирующий клиент, сгенерированный по ней,
|
||||
отверг бы законный ответ ре-прохода. Сверено по проводу: `runOrderedChapters` для ре-прохода
|
||||
отдаёт `ceiling_chapters`, а он ноль. ⇒ `minimum: 0`. ⚠ Нижняя граница НЕ снята, хотя `null` уже
|
||||
отдельная ветвь типа: ноль — это ПОЛ, а не «сегодняшнее наименьшее», отрицательное отвергает сам
|
||||
допуск стора, и схема теперь говорит вслух то, что сервис и так гарантирует.
|
||||
⚠ **Пина на это не было — завела.** `pgstore.TestARePassRunsBarIsOneUnitOfWork` расширен: он
|
||||
держал только полосу `0/1 re_pass`, теперь держит и `ordered_chapters: 0`, и `ordered_units: null`,
|
||||
и `delivered_chapters: 0` — **на ОБЕИХ поверхностях**, стартовой квитанции и пути чтения, потому
|
||||
что это два разных SQL-выражения. Мутация (`delivered_chapters` → null) ловится.
|
||||
⚠ **Это ВОЗВРАТ, а не сочинение — эррата `06.09-г`.** Верная граница `minimum: 0` и абзац о ней в
|
||||
каноне УЖЕ СТОЯЛИ; их снёс один заход оркестратора, писавший канон 0.12.0, — он заменил `0` на `1`
|
||||
и пере-написал прозу в редакцию, говорящую ровно то же самое. То есть половина поля, которую
|
||||
читает ЧЕЛОВЕК, всё это время держала правду, а половина, которую читает ГЕНЕРАТОР КЛИЕНТА, —
|
||||
ложь, и стояли они в шести строках друг от друга. Разница для урока существенна: не «зона написала
|
||||
правило», а «зона вернула правило, снесённое вместе с прозой, которая его же и повторяла». ⚠ Свою
|
||||
сверку я вела от ПРОВОДА и ПИНА, а не от его письма, поэтому вывод от авторства не зависел.
|
||||
|
||||
2. **Абзац `Progress` объявлял единственным различителем `Run.delivered_chapters`, и это ложь.** Для
|
||||
ре-прохода `runDelivered` отдаёт `0` — ЧИСЛО, — значит по этому правилу клиент прочёл бы ре-проход
|
||||
как ГЛАВЫ и нарисовал «0 of 1 chapters», ровно то, что абзац запрещает двумя строками ниже.
|
||||
⇒ переписано на пару, описывающую ЗАКАЗ, с явным порядком проверок: `ordered_chapters == 0` ⇒
|
||||
ре-проход · `== null` ⇒ юниты · иначе главы. Порядок несущий: проверка ре-прохода обязана быть
|
||||
ПЕРВОЙ.
|
||||
⚠ **Отклонена вторая половина предложения оркестратора** — он назвал `stage: re_pass` равноправной
|
||||
альтернативой. `stage` — ОТКРЫТЫЙ словарь (новые значения версию не двигают), и клиент обязан ему
|
||||
деградировать, а не решать по нему. В канон вписано «corroborates; does not discriminate».
|
||||
|
||||
⛔ **И КРАЙ, КОТОРЫЙ ВСКРЫЛСЯ ТОЛЬКО ОТ ЭТОЙ ПРАВКИ — правило эксклюзивности едва не оказалось
|
||||
неверным.** Канон теперь утверждает «ноль ⇒ ре-проход и ничто иное». Проверено: `pricing.Quote`
|
||||
клампит заказ всей книги к остатку, поэтому над ПОЛНОСТЬЮ ДОСТАВЛЕННОЙ книгой он квотит **ноль глав**,
|
||||
а допуск стора отвергает ноль только при `Resnapshot == false` — которое любое продолжение над
|
||||
сдвинутым банком ставит в `true`. То есть без гварда `quote.Chapters == 0` в `runs.Start` такой прогон
|
||||
лёг бы с `ceiling_chapters = 0` и был бы НЕОТЛИЧИМ от ре-прохода на проводе: полоса `0/1`,
|
||||
`stage: re_pass`, `delivered_chapters: 0`. Гвард есть и стоит верно — ПОСЛЕ ветви ре-прохода и ДО
|
||||
взятия холда, — но **не был пиньнут ничем**. Заведён
|
||||
`runs.TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun`; мутация (снятие гварда)
|
||||
ловится, деньги при отказе не двигаются.
|
||||
|
||||
⚠ **Чего НЕ обнаружилось:** расхождения на стороне ПРОВОДА. Оба раза лгал документ. `delivered_chapters: 0`
|
||||
у ре-прохода — не дефект: он купил ноль глав и доставил ноль, и `ordered_chapters: 0` рядом делает
|
||||
это чтение однозначным; `null` там был бы ХУЖЕ, потому что `null` уже занят под «меряется в юнитах».
|
||||
|
||||
### ⛔ ОТДЕЛЬНЫЙ АКТ 06.09 — ПРИБОР ПЕРЕСТАЛ УМЕТЬ ВЫДАВАТЬ ЧИСТУЮ СПРАВКУ О ТОМ, ЧЕГО НЕ ИЗМЕРЯЛ
|
||||
|
||||
Заказан оркестратором отдельно от дофикса по канону, ПОСЛЕ его закрытия — чтобы не смешивать предметы
|
||||
посреди лендинга. Ряд регистра **`PD-454`**.
|
||||
|
||||
**ЧТО БЫЛО.** Каждая строка, которую `make check` печатает о прогоне — список пакетов, ряды `ALARM`,
|
||||
список падений, счёт скипов, — есть ГРЕП по одному файлу. Греп отвечает на ОТСУТСТВУЮЩИЙ файл ровно
|
||||
тем же, чем на чистый: ничем. Поэтому рецепт доходил до финального `else` и печатал
|
||||
«every test ran: no host condition was missing» о прогоне, чей лог исчез. **Наблюдено, а не выведено:**
|
||||
три подряд `grep: .check.log: No such file or directory`, следом эта самая строка — на прогоне, где
|
||||
скипов было ПЯТЬ. Код возврата 0, потому что статус берётся от `go test` ДО грепов.
|
||||
|
||||
**ПРИЧИНА.** Фиксированное имя лога, общее для всех прогонов в каталоге. Два прогона в одном каталоге
|
||||
здесь НОРМА — батарею гоняют и зона, и оркестратор, — и первый закончивший удаляет улику второго
|
||||
посреди рецепта.
|
||||
|
||||
**ЛЕЧЕНИЕ ДВУМЯ ПОЛОВИНАМИ, и они не дублируют друг друга.** Имя лога стало ПОПРОГОННЫМ (`$$` — PID
|
||||
шелла): снимает сегодняшнюю ПРИЧИНУ. Гвард «нет лога ⇒ выйти красным ДО первого чтения»: снимает
|
||||
КЛАСС — отказавший редирект, полный диск, рука. ⚠ Проверено ИСПОЛНЕНИЕМ, а не рассуждением: подменённый
|
||||
`GO`, который не пишет лога, даёт красный выход и две строки объяснения вместо справки.
|
||||
|
||||
**ГЕЙТ ДЕРЖИТ ОДНУ БЕЗОПАСНУЮ ФОРМУ И ГОВОРИТ ЭТО ПРЯМО** —
|
||||
`gates.TestTheBatteryCannotReportCleanlinessWithoutItsLog`: проверка лога, затем выход с ЛИТЕРАЛЬНЫМ
|
||||
ненулевым кодом, затем чтения. Внутри формы детали свободны (имя попрогонное или нет, `-s`/`-f`/`-e`),
|
||||
а иная — пусть и верная — форма покраснеет НАМЕРЕННО: пере-кроить единственный рецепт, через который
|
||||
отчитывается вся батарея, должен человек и пере-пиньнуть, а не проскочить молча. Имя лога гейт читает
|
||||
ИЗ РЕЦЕПТА (из редиректа), чтобы переименование не заставило его смотреть на другой файл и промолчать.
|
||||
**ПЯТЬ мутаций ловятся, все проверены исполнением:** гвард удалён · гвард после первого чтения · тест
|
||||
есть, `exit` убран · `exit 1` → `exit 0` · `exit` без кода.
|
||||
|
||||
⛔ **ВТОРАЯ РЕДАКЦИЯ ГЕЙТА, И НАШЛА ЕЁ НЕ Я, А ПРИЁМКА.** Первая требовала лишь, чтобы после гварда был
|
||||
шаг, начинающийся с `exit`, — и `exit 0` проходил ЗЕЛЁНЫМ. Оркестратор замерил исполнением: с `exit 0`
|
||||
и подменённым `GO` батарея печатает «THE BATTERY LEFT NO LOG» и **возвращает `MAKE-EXIT = 0`**.
|
||||
Воспроизведено мной, файл восстановлен по sha256. ⇒ **тот же дефект, переодетый, и в одном отношении
|
||||
ХУЖЕ исходного:** прежняя ложная справка была СТРОКОЙ, которую человек ловит глазами, эта — КОД
|
||||
ВОЗВРАТА, который потребляет машина: CI, скрипт, лендинг. ⚠ И моё собственное сообщение об ошибке
|
||||
обещало «so this exits RED», проверяя лишь НАЛИЧИЕ выхода, — та же болезнь, что гейт лечит, внутри
|
||||
него самого; формулировка подтянута под проверяемое. Теперь требуется ЛИТЕРАЛ: голый `exit` несёт
|
||||
статус предыдущей команды (`echo`, то есть ноль), а `exit $var` из рецепта не судится вовсе — оба
|
||||
отвергаются, а не угадываются. Сквозная проверка после правки: `MAKE-EXIT=2`, громкая строка
|
||||
напечатана один раз, ложная справка — ноль раз.
|
||||
|
||||
⚠ **И ГЕЙТ ПОЙМАЛ САМ СЕБЯ.** Его первая редакция считала ЧТЕНИЕМ слово `grep` внутри объясняющего
|
||||
`echo` того самого гварда — и объявила гвард опоздавшим. Это ровно тот substring-vs-invocation капкан,
|
||||
о котором соседний гейт в этом же файле пишет отдельным абзацем; я в него вошла, цитируя его.
|
||||
Поймано прогоном на второй минуте, не рассуждением. ⇒ различается ВЫЗОВ, а не слово.
|
||||
|
||||
⚠ **Заодно снят второй носитель одной грамматики:** разбор рецепта Makefile был написан внутри
|
||||
соседнего гейта, и вторая копия его же в новом тесте была бы ровно тем «вторым носителем, свободным
|
||||
разойтись», который этот пак вычищал из денежного пути. Вынесен в `makeRecipe(t, target)`, оба гейта
|
||||
читают один разбор; прежний тест зелен без правок.
|
||||
|
||||
### ⛔ ТРЕТИЙ ЗАХОД ПО НОРМЕ АВТОРСТВА — ОДНА ЭРА, ТРИ НОСИТЕЛЯ, ПРОТУХШИЕ ПО-РАЗНОМУ
|
||||
|
||||
Найдено не зоной: строитель нашёл, оркестратор проверил, зона пишет из пина (`D39.211` п.3а).
|
||||
|
||||
**⑴ `Book.structure` и `OrderOptions.structure` несли РАЗНОЕ, хотя канон утверждал обратное.**
|
||||
В `OrderOptions.structure` я записала «the sibling `Book.structure` carries the same fact and says the
|
||||
same in its own words; **the two cannot disagree**». Проверено — на тот момент это было заявление о
|
||||
НАМЕРЕНИИ, а не о тексте: `Book.structure` говорил только «`null` while the book is still arriving» и
|
||||
не различал `null` и `none` ВОВСЕ. ⇒ правило «нет провенанса ≠ есть, и он плохой» жило в схеме ЗАКАЗА,
|
||||
а поле, которое клиент видит на карточке КНИГИ, о нём молчало.
|
||||
|
||||
**⑵ И «still arriving» ýже факта.** Книга может приехать целиком и отвечать `null`, потому что разрез
|
||||
не гоняли. Это не рассуждение: пин, на котором стоит правило, берёт книгу в статусе `not_started` —
|
||||
полностью загруженную, без манифеста — и получает явный `null`.
|
||||
|
||||
**⑶ Третий носитель той же эры — МОЙ КОД.** `httpapi.wireBook.Structure` перечислял ТРИ значения
|
||||
(`declared`/`detected`/`none`) и `delimited` не знал: комментарий остался от трёхзначной эры и читался
|
||||
как обещание. ⇒ **три носителя одной эры протухли по-разному, и один из них мой.** Перечень из
|
||||
комментария УБРАН совсем: словарь открытый и растёт аддитивно, список в комментарии протухает молча —
|
||||
канон держит слова, комментарий держит форму.
|
||||
|
||||
**ПИН ВЫДЕЛЕН, А НЕ ПРОЦИТИРОВАН ЧУЖОЙ.** Утверждение о `null` на карточке книги держалось внутри
|
||||
`TestTheCharacterCountSaysWhichNumberItIs` — постояльцем без своего имени, а канон, цитирующий пин по
|
||||
имени, не может цитировать пин, названный про другое. Перенесено (не скопировано) в
|
||||
`httpapi.TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull`, туда же добавлено третье
|
||||
утверждение: незнакомое слово (`delimited`) едет ВЕРБАТИМ, потому что сборка, переписывающая
|
||||
непонятое, спрятала бы рост словаря от клиента, который обязан на нём деградировать.
|
||||
|
||||
⚠ **И правило, которое зона взяла себе из этого:** смысл значения нельзя списывать с комментария
|
||||
провода — сегодня это ВТОРОЙ случай, когда так списанное утверждение оказалось ложным. Источник —
|
||||
ПИН.
|
||||
|
||||
⛔ **И ТУТ ЖЕ — ЧЕТВЁРТЫЙ СЛУЧАЙ, ТОНЬШЕ ВСЕХ ПРЕДЫДУЩИХ: ПИН БЫЛ, А УТВЕРЖДЕНИЕ ВСЁ РАВНО УЕХАЛО.**
|
||||
Я написала в канон, что `null` бывает у книги «fully uploaded, no manifest», и сослалась на пин, чей
|
||||
фейк отдаёт `not_started` без структуры. Пин настоящий и держит РОВНО ТО, ЧТО ДЕРЖИТ: пустая колонка
|
||||
доезжает до клиента явным `null`. Но это **тест ОТОБРАЖЕНИЯ** — он ничего не говорит о том, бывает ли
|
||||
такая пара у настоящей книги. Я прочитала ФИКСТУРУ как продуктовое состояние. Поймал оркестратор
|
||||
сверкой с соседним словарём: `BookStatus` обещает у `not_started` «cut», то есть манифест прочитан.
|
||||
|
||||
⇒ **Ответ дал пин уровня ХРАНИЛИЩА, заведённый под этот вопрос** —
|
||||
`pgstore.TestAParsedBookIsNotStartedBeforeItHasAnyProvenance`. `FinishParse` ставит `not_started` и
|
||||
ВЗВОДИТ ДОЛГ НА ДЕРЕВО в одной транзакции (его собственный комментарий: «this is the transaction that
|
||||
makes the book parsed, and a book that is parsed owes a tree»). ⇒ окно «разрезана, провенанса ещё нет»
|
||||
СПРОЕКТИРОВАНО, а не гонка. Ложной оказалась **моя иллюстрация**, а не словарь статусов и не
|
||||
различение `null`/`none`: `not_started` говорит, что разрезан ИСХОДНИК, а не что прочитан манифест.
|
||||
Три дороги к тому же `null`: долг ещё не оплачен · манифест старше поля · книга заведена с уже
|
||||
лежащего файла.
|
||||
|
||||
⚠ **Урок, который стоит отдельно от находки:** «есть пин» — не то же, что «пин доказывает сказанное».
|
||||
Доказано было ОТОБРАЖЕНИЕ, сказано — про ЖИЗНЬ. Цитируя пин в утверждении о значении, спрашивать не
|
||||
«зелен ли он», а «то ли он мерит».
|
||||
|
||||
### ⛔ ЧЕТВЁРТЫЙ АКТ ПО ПРИБОРУ — ПОЧИНКА ЗАБРАЛА СВОЙСТВО, КОТОРОГО НИКТО НЕ НАЗЫВАЛ (`PD-458`)
|
||||
|
||||
⚠ **Класс — MINOR, и раздувать его до крита я не стала.** Файлы нулевые, в коммит не попадают, ни
|
||||
денег, ни данных, ни лжи на проводе. Но чинится тремя строками, а откладывать трёхстрочную правку
|
||||
церемонией — это и есть «экономия усилия на неудобном»; владелец назвал это прямо, и он прав.
|
||||
|
||||
**ЧТО ЗАБРАЛА ПОЧИНКА `PD-454`.** Фиксированное имя лога само себя ограничивало: сколько бы прогонов
|
||||
ни падало, оставался ОДИН несвежий файл, следующий его перезаписывал. Попрогонное имя убрало коллизию
|
||||
ВМЕСТЕ с этим ограничением. Замер, а не опасение: за ночь накопилось ЧЕТЫРЕ файла. ⇒ гниёт ровно та
|
||||
читаемость, ради которой лог и сохраняется: рецепт НАРОЧНО держит лог упавшего прогона, чтобы его
|
||||
прочли, и куча мусора от прерванных прогонов от него неотличима.
|
||||
|
||||
**ПОДМЕТАНИЕ УСЛОВНО ПО ЖИВОСТИ ПИСАТЕЛЯ** (`kill -0` — POSIX, без `/proc`, так что линуксовая
|
||||
оговорка, которую я назвала первой редакцией, снята). Голое `rm -f .check.log.*` снесло бы лог
|
||||
ПАРАЛЛЕЛЬНОГО прогона и вернуло бы ту самую коллизию. Перезанятый PID оставляет файл — консервативное
|
||||
направление, единственное, что не уничтожает улику. Проверено исполнением: живой лог уцелел, мёртвый
|
||||
снят, нечисловой хвост пропущен.
|
||||
|
||||
**ГЕЙТ `TestTheBatterySweepsOnlyLogsWhoseWriterIsGone`** держит форму «удалять можно СВОЙ лог и ничей
|
||||
больше»; три мутации ловятся — голое `rm -f`, цикл без проверки живости, подметание убрано совсем.
|
||||
|
||||
⛔ **И ДВА СОБСТВЕННЫХ ДЕФЕКТА ЭТОГО ЖЕ ГЕЙТА, пойманных прогоном, а не чтением:**
|
||||
1. **Третий раз за смену — слово вместо команды.** Проверка `strings.Contains(s, "rm ")` сработала на
|
||||
`echo`, где «ala**rm m**arkers». Соседний гейт предупреждает об этом капкане ПРОЗОЙ, я в него вошла
|
||||
в третий раз. ⇒ вынесено в НАЗВАННУЮ функцию `removesAFile`, через которую обязана идти любая такая
|
||||
проверка: прозаические предупреждения этот класс явно не останавливают, у функции есть шанс.
|
||||
2. ⛔ **Мутация покраснела ВЕРНОЙ строкой по НЕВЕРНОЙ причине.** `@rm -f .check.log.*` — make клеит
|
||||
префикс тишины к первой команде рецепта, первый токен `@rm`, и детектор её не увидел; гейт сказал
|
||||
«никогда не удаляет старый лог» о рецепте, который первым же действием удалял их ВСЕ. Вердикт был
|
||||
бы засчитан как «мутация поймана». **Правый вердикт по неправой причине — дыра, а не пропуск**, и
|
||||
отличается он от настоящего только тем, что кто-то прочёл ТЕКСТ сообщения, а не его цвет.
|
||||
|
||||
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
|
||||
|
||||
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
|
||||
**всем 193 свойствам схем канона**, три правила: граница `minimum > 0` при прозе, говорящей про ноль ·
|
||||
проза обещает `null`, а тип его не допускает · тип допускает `null`, а проза о нём молчит. Семь
|
||||
флагов, **пять — ложные срабатывания** эвристики (`target` прямо говорит «never `null`»;
|
||||
`RunRequest.chapters`/`characters` несут `minimum: 1` при прозе, которая ноль ЗАПРЕЩАЕТ;
|
||||
`character_count_exact` и `kind` упоминают `null` СОСЕДНЕГО поля). Настоящих два:
|
||||
|
||||
1. ⛔ **`OrderOptions.structure` — тип `oneOf [string, null]`, а проза не говорила, КОГДА бывает `null`
|
||||
и что он значит.** Зеркало разобранного случая: про null знал генератор, не знал человек. Провод
|
||||
его шлёт — `wireOrderOptions.Structure *string`, «null when no manifest has answered yet»
|
||||
(`internal/httpapi/v0.go`, греп `Structure \*string`), — и это пиньнуто:
|
||||
`httpapi.TestABookWhoseCutCannotBeSoldAgainstOffersCharactersAndSaysSo` проверяет ЯВНЫЙ `null`, а
|
||||
не выдуманное слово. Смысл дописан из пина: **`null` — не четвёртый вид разреза и не `none`**;
|
||||
`none` — книга, на которую движок посмотрел и нашёл одну главу, `null` — книга, которую он ещё не
|
||||
спрашивали. Соседний `Book.structure` то же самое говорил, а этот молчал.
|
||||
2. ⚠ **`Chapter.number` — `minimum: 1` при `type: [integer,'null']` и прозе «`null` законен». НЕ
|
||||
ТРОНУТО:** противоречие уже объявлено ЭРРАТОЙ 05.09 внутри самого поля, с верным разбором и
|
||||
адресом (структурный пак). Сверено, что довод эрраты ЖИВ на моей стороне: `ingest.Whole()`
|
||||
по-прежнему отвергает `c.Number < 1` (`internal/ingest/manifest.go`, греп `chapter numbering is
|
||||
1-based`), то есть книга, которую абзац называет законной, через шов не проходит. Добавлять нечего.
|
||||
|
||||
⚠ `ordered_units` с `minimum: 1` проверен оркестратором и верен (заказа на ноль юнитов нет, «не
|
||||
юнитами» несёт `null`) — не тронут.
|
||||
|
||||
### ⚠ И НАХОДКА О САМОМ СПОСОБЕ ПЕРЕДАЧИ, отданная оркестратору
|
||||
|
||||
**Номер строки, посланный сообщением, протухает от следующей правки того же файла и ничем не
|
||||
сторожится.** Я дала ему три номера для чужих якорей, потом сама же дописала в канон два блока — и все
|
||||
три соврали на четыре строки. Поймала это тем, что пере-считала перед отправкой итога, а не тем, что
|
||||
что-то покраснело. ⇒ надёжная форма передачи — ТОКЕН, а не номер: `counts.py --lint` сверяет по
|
||||
СОДЕРЖИМОМУ и единственный способен сказать, попал ли пере-нацеливший.
|
||||
|
||||
⚠ **Мои правки канона сдвинули ТРИ чужих якоря** (`docs/PROGRESS.md:237` и `:244`,
|
||||
`frontend/docs/frontend-PROGRESS.md:162`) — все целят в канон. Чужие журналы не трогаю; точные новые
|
||||
строки отданы оркестратору вместе с ТОКЕНАМИ, чтобы он пере-нацелил их тем же лендингом и пере-считал
|
||||
номер линтом в момент правки, а не взял мой из письма.
|
||||
|
||||
✅ **ЧТО ПРОВЕРЕНО КАК ЗАКРЫТОЕ ОРКЕСТРАТОРОМ** (по его дереву, а не со слов): канон `info.version:
|
||||
0.11.0` · `delivered_chapters` нуллабелен и объяснён · `ordered_chapters: minimum 0` с объяснением
|
||||
нуля · `ceiling_chapters` deprecated + «REFUSED 400» · `run_limit_reached` пере-формулирован ТОЧНЕЕ
|
||||
моего («спустил проданный заказ», а не «доставил всё») · строка 297 уже несёт form feed · строка 301
|
||||
(`--max-units` и carried) заведена · строка 304 (NULLABLE денежные колонки, с историей ложной причины)
|
||||
заведена · строка 280 закрыта с НАЗВАННОЙ непостроенной половиной.
|
||||
|
||||
### ⛔ ТРЕТИЙ КРУГ — ДВЕ ЛИНЗЫ ПРИЁМКИ ОРКЕСТРАТОРА, и они нашли ЦЕНТРАЛЬНОЕ
|
||||
|
||||
**Пак научился читать у движка цену, но не научился считать в его ЕДИНИЦАХ.** Заказ, холд и
|
||||
|
|
@ -265,6 +601,12 @@ NULLABLE денежных колонок (сегодня не задета: ру
|
|||
| 6 | `internal/pgstore/runs.go:1505` · перезапущенный прогон респавнится без заказа | `TestTheVolumeAllowanceIsWhatIsLeftOfTheOrderAtEverySpawn` |
|
||||
| 7 | `internal/runs/spawn.go:329` · неразрешимый заказ спавнится как «вся книга» | `TestAnOrderThatNoLongerNamesABoundaryRefusesToSpawn` |
|
||||
| 8 | `internal/runs/runs.go:373` · неоценённая книга продаётся вместо отказа | `TestAnUnpricedBookIsRefusedRatherThanSoldAtAGuess` |
|
||||
| 9 | `internal/runs/spawn.go` · ЕДИНИЦА ИЗМЕРЕНИЯ: `o.Units - o.Delivered` → `l.OrderedChapters - o.Delivered` | `TestTheVolumeAllowanceCountsUnitsAndNotChapters` |
|
||||
|
||||
⚠ **Посадка №9 добавлена ПОСЛЕ приёмки и только вместе с фикстурой `multiUnitBook`:** до неё во всей
|
||||
зоне не было главы больше чем из одного юнита, поэтому подмена единицы измерения проходила батарею
|
||||
зелёной, и восемь посадок эту ось не щупали вовсе. Порядок был именно такой — сначала фикстура, потом
|
||||
посадка, потом лечение.
|
||||
|
||||
⚠ Посадка №1 красит 34 теста — широко, потому что тестовый хелпер `fixtureHold` считает холд
|
||||
НЕЗАВИСИМО от продакшн-кода, и расхождение видно всюду, где утверждается сумма. Это обнаружение, а не
|
||||
|
|
@ -272,6 +614,22 @@ NULLABLE денежных колонок (сегодня не задета: ру
|
|||
|
||||
### ЗАЯВЛЕНИЕ О ПРАВКАХ ТЕСТОВ (`D39.183`) — что изменилось, что это описывало, куда уехала гарантия
|
||||
|
||||
0. ⛔ **ПИН, КОТОРЫЙ Я ЖЕ ЗАВЕЛА ЧАСОМ РАНЬШЕ, ПЕРЕВЁРНУТ — 06.09, и это самая неудобная запись здесь.**
|
||||
`TestACharacterOrderReportsTheChapterSpanItReachesIntoAndNotZero` фиксировал, что символьный заказ
|
||||
публикует `ordered_chapters` = СПАН (замер: 2 при пяти купленных юнитах из двенадцати). Я завела
|
||||
его, чтобы описание не уехало обратно, — то есть **закрепила поведение, которое сама же через час
|
||||
признала дефектом**. Что изменилось в поведении: `ordered_chapters` для такого прогона стал `null`,
|
||||
рядом появился `ordered_units`. Что описывал старый тест: величину спана и её ненулевость. Куда
|
||||
уехала гарантия: в `TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn` (обе половины пары —
|
||||
null у одной, 5 у другой) и в `TestAChapterOrderKeepsTheBarItAlwaysHad`, куда добавлена ОБРАТНАЯ
|
||||
половина: у главного заказа `ordered_chapters` = 2, `ordered_units` = nil. Имя тоже сменено — старое
|
||||
стало бы ложью в списке тестов. Заказано ратификацией оркестратора после окея коллеги-Fable, не
|
||||
зеленью: старый пин был ЗЕЛЁН и остался бы зелёным.
|
||||
⚠ **Оба новых пина проверены МУТАЦИЯМИ, а не объявлены рабочими:** публикация спана снова красит
|
||||
символьную половину, безусловный `null` красит главную. Дерево восстановлено и зелено.
|
||||
⚠ **Урок для читателя:** пин, заведённый в тот же заход, что и находка, закрепляет ПОНИМАНИЕ этого
|
||||
захода, а не истину. Мой закрепил величину, не спросив, ту ли величину мы публикуем.
|
||||
|
||||
1. **`internal/pricing/pricing_test.go` переписан ЦЕЛИКОМ.** Он описывал шкалу в главах и ставку
|
||||
(`Scale`, `Ceiling(глав)`, `DefaultPerChapter`, полосу провенанса $0.0252–0.0274) — всё это
|
||||
удалено строкой 280. Гарантии уехали: «максимум = баланс КАК ЕСТЬ, холды не вычитаются дважды» →
|
||||
|
|
@ -329,10 +687,32 @@ NULLABLE денежных колонок (сегодня не задета: ру
|
|||
|
||||
### СОСТАВ КОНТРАКТНОГО МИНОРА — НА РАТИФИКАЦИЮ ОРКЕСТРАТОРУ (канон своей рукой НЕ трогала)
|
||||
|
||||
`info.version` **0.10.0 → 0.11.0**. Сборка уже объявляет `0.11.0`
|
||||
(`internal/httpapi/capabilities.go`), поэтому гейт `TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
|
||||
КРАСНЫЙ по истинной причине — пара не закрыта. Порядок лендинга ратифицирован: код первым, канон
|
||||
вторым.
|
||||
`info.version` **0.10.0 → 0.11.0** ✅ **ЗАКРЫТО ОРКЕСТРАТОРОМ 06.09, сверено** — а затем
|
||||
**0.11.0 → 0.12.0**, и этот минор прожил несколько часов.
|
||||
|
||||
⛔ **ПОЧЕМУ 0.11.0 ЖИЛ ЧАСЫ — чтобы следующая смена не гадала.** Пятый заход нашёл в нём две лжи и
|
||||
исправил ВТОРУЮ поведением: `Run.ordered_chapters` стал нуллабельным, рядом приехало `ordered_units`
|
||||
(разбор — блоком ниже). Это смена ОТДАВАЕМОГО ПРОВОДА, а правило у самой константы велит поднимать
|
||||
номер тем же изменением, что и код, «never as a courtesy afterwards».
|
||||
|
||||
⚠ **Решение принято НЕ мной и не молча — и след его честен.** Зона поставила вопрос оркестратору,
|
||||
назвав СВОЙ интерес («внутрь дешевле, константу править не надо»). Оркестратор ответил
|
||||
ПРЕДВАРИТЕЛЬНО «внутрь 0.11.0», опираясь на то, что сам правил текст этого минора трижды после
|
||||
лендинга и не бампал, — и **отозвал свой же ответ**, проверив те три: открытый словарь, выросший на
|
||||
значение (канон прямо говорит, что версию не двигает), ⛔-пометка дефекта (проза) и ЭРРАТА,
|
||||
исправлявшая ложь канона о членах, которые провод и так нёс. Ни одна не была сменой ФОРМЫ ⇒ бамп за
|
||||
них не причитался, непоследовательности нет. Плюс его же критерий «минор закрыт с момента акта
|
||||
приёмки» — акт `D39.208` и есть акт приёмки 0.11.0. Окончательный ответ: **0.12.0**.
|
||||
⚠ Мой довод против бампа («номер начнёт считать НАШИ заходы») верен для прозы и эррат и потому здесь
|
||||
не применяется: правило константы говорит про КОД И ПРОВОД, а не про число читателей.
|
||||
|
||||
⇒ **гейт `TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` СНОВА КРАСЕН, и это верно:** порядок
|
||||
ратифицирован — код первым, канон вторым (эррата 04.09-в: обратный порядок стоил суток лжи на
|
||||
проводе). Гасит его канон, а не подгонка. Позиции НИЖЕ, помеченные ⛔, в канон ещё не приехали.
|
||||
|
||||
⚠ **И дыра гейта, найденная тем же вопросом и заведённая оркестратором строкой бэклога 309:** он
|
||||
сверяет ВЕРСИИ, а не ФОРМЫ. Все три сегодняшние лжи канона он пропустил именно поэтому — число
|
||||
совпадало. Бамп не чинит дыру, он только не даёт номеру солгать.
|
||||
|
||||
**`RunRequest`** — `ceiling_chapters` УДАЛЁН; `chapters: integer|null` (сколько ОСТАВШИХСЯ глав) ·
|
||||
`characters: integer|null` (сколько исходного текста в рунах) · `re_pass` как был. Взаимно
|
||||
|
|
@ -359,8 +739,73 @@ blocked: Blocked | null
|
|||
клиент обязан деградировать на незнакомом слове, а не отказывать. `covers_none` при `chapters_left: 0`
|
||||
это ЗАКОНЧЕННАЯ книга, при положительном — счёт, не покрывающий следующую главу.
|
||||
|
||||
**`Run`** — `ceiling_chapters` → `ordered_chapters`; добавлены `delivered_chapters: integer` и
|
||||
`term_consistency_funded: boolean`.
|
||||
**`Run`** — `ceiling_chapters` → `ordered_chapters`; добавлены `delivered_chapters: integer|null`,
|
||||
`term_consistency_funded: boolean` и (правкой 06.09, ниже) `ordered_units: integer|null`.
|
||||
**Залендено оркестратором в 0.11.0, сверено 06.09** (`openapi.yaml:1827-1835`).
|
||||
|
||||
⚠ **`null` у `delivered_chapters` — не «ноль», и ОБОСНОВАНИЕ его в каноне НЕВЕРНО.** Канон говорит:
|
||||
«an order expressed in CHARACTERS stops inside a chapter and **closes none**». Измерено — закрывает:
|
||||
`UnitsFor` возвращает первый юнит, чья нарастающая сумма покрывает запрошенное, поэтому заказ садится
|
||||
ровно на границу главы примерно так же часто, как главы делят юниты (при трёх юнитах на главу —
|
||||
каждый третий), и такой заказ закрывает целые главы, оставаясь юнитовым. Мой собственный пин
|
||||
`TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn` покупает всю главу один и ещё юнит главы
|
||||
два. ⇒ верное обоснование: `null` стоит потому, что заказ **ВЫРАЖЕН В ЗНАКАХ и меряется в них
|
||||
насквозь**, а не потому, что он «не закрывает глав». Довод за такое поведение: две покупки, которые
|
||||
человек не отличает друг от друга, не должны рисоваться по-разному, а при определении «по факту
|
||||
попадания» одинаковые на вид заказы получали бы то юнитовую полосу, то главную. ⚠ Это ЛОЖНОЕ
|
||||
ОПРЕДЕЛЕНИЕ жило в ВОСЬМИ носителях; семь моих исправлены (`pricing.Quote.UnitShaped`, два места
|
||||
`runs.go`, `unitShapedOrder`, `pgstore.StartRunInput.OrderedUnits`, `httpapi.wireRun.DeliveredChapters`,
|
||||
этот абзац состава), восьмой — канон, в двух его местах, отдан оркестратору.
|
||||
|
||||
⛔ **`Progress` — ТРЕТИЙ СЛУЧАЙ, КОТОРОГО В СОСТАВЕ НЕ БЫЛО И В КАНОНЕ 0.11.0 НЕТ. Внесено пятым
|
||||
заходом 06.09; до него состав нёс мою ложную фразу «полоса осталась в главах».** Канон говорит «in
|
||||
chapters» в шапке схемы и в обоих счётчиках (`openapi.yaml:1449,1479,1483). Сборка для заказа,
|
||||
выраженного ЗНАКАМИ, шлёт по `done`/`total` **ЮНИТЫ** (`readmodel.go:632,636`, ветвь
|
||||
`when r.ordered_units is not null`; пин `runs.TestACharacterOrdersBarIsCountedInWhatItActuallyBought`).
|
||||
⇒ клиент, читающий канон, нарисует «3 из 10 глав» там, где это 3 из 10 юнитов. Требуется абзац той же
|
||||
формы, что уже стоит у РЕ-ПРОХОДА («declared shape, not chapters — do not render it as “0 of 1
|
||||
chapters”»), плюс **имя различителя**. Различитель на проводе ОДИН: **`delivered_chapters: null` ⇒
|
||||
полоса юнитовая**; `delivered_chapters: 0` при `ordered_chapters: 0` ⇒ ре-проход; `delivered_chapters`
|
||||
числом ⇒ главы. Сегодня он существует, но нигде не назван, и клиент обязан его УГАДАТЬ.
|
||||
|
||||
⛔ **ВТОРАЯ ЛОЖЬ В ТОЙ ЖЕ ЗАЛЕНДЁННОЙ СХЕМЕ — НАЙДЕНА, ИЗМЕРЕНА И ПОЧИНЕНА ПОВЕДЕНИЕМ.**
|
||||
`Run.ordered_chapters` описан как «a re-pass buys no chapters, and an order phrased in CHARACTERS spans
|
||||
no whole chapter — **both report `0` here**». Для символьного заказа это было неверно: `resolveOrder`
|
||||
звал `QuoteUnits(…, chaptersSpanning(pb, n))`, а `QuoteUnits` ставит `Chapters: max(chapters, 1)` ⇒
|
||||
поле было ВСЕГДА ≥ 1 и означало не «сколько глав заказано», а «скольких глав заказ КАСАЕТСЯ».
|
||||
|
||||
**ЗАВЫШЕНИЕ ЗАМЕРЕНО, И ОНО ХУДШЕЕ НА САМОМ ДЕШЁВОМ ЗАКАЗЕ:** один юнит из четырёх в первой главе
|
||||
публиковался как «1 глава» — сто процентов завышения ровно на том заказе, которым сервис пробуют
|
||||
впервые. Зона сперва защищала спан доводом «это единственная величина, по которой видно, докуда дошли
|
||||
деньги», **и сама этот довод сняла**: `runs.ordered_units` лежит колонкой с миграции 00033 и уже
|
||||
проецируется латералью `lastRun` — наружу не выходил только по умолчанию. Посылка была ложной.
|
||||
|
||||
⇒ **ФОРМА ИЗМЕНЕНА (ратифицировано оркестратором 06.09 после окея коллеги-Fable):**
|
||||
```
|
||||
ordered_chapters: integer|null // null, когда прогон продан НЕ в главах
|
||||
ordered_units: integer|null // НОВОЕ: сколько юнитов купил символьный заказ
|
||||
```
|
||||
Ровно ОДНО из двух заполнено, и какое именно — говорит, в какой единице меряется весь прогон: полоса,
|
||||
доставка, остаток. Различитель перестаёт быть угадыванием.
|
||||
|
||||
⚠ **НУЛЛИТСЯ ПРЕДСТАВЛЕНИЕ, А НЕ КОЛОНКА** (`pgstore.runOrderedChapters`). `ceiling_chapters = 0` —
|
||||
собственная метка РЕ-ПРОХОДА, и её читают ТРИ внутренних места: `runs.maxUnitsFor` (`LiveRun`),
|
||||
резюм-гейт реконсилятора (`LiveRun`) и допуск самого стора (`StartRunInput`). Запись нуля или null в
|
||||
СТРОКУ сделала бы символьный заказ неотличимым от ре-прохода для всех трёх, и у первого цена — деньги:
|
||||
ноль есть движковое слово «без объёмного предела вовсе». ⚠ Ревью предлагало пере-ключить эти три на
|
||||
`OrderedUnits != nil`; зона ОТКАЗАЛАСЬ и отказ приняли: они спрашивают «ре-проход ли это», а не
|
||||
«символьный ли заказ», и переключение инвертировало бы вопрос — у ре-прохода `OrderedUnits` nil, и
|
||||
ветвь не сработала бы НИКОГДА. Пояс `&& OrderedUnits == nil` тоже НЕ поставлен, по доводу
|
||||
оркестратора: гейт, защищающий от несуществующего состояния, завтра прочтут как свидетельство, что
|
||||
состояние бывает.
|
||||
|
||||
⚠ **Первый различитель, который зона отдала оркестратору письмом, опирался на обещанный каноном нуль и
|
||||
потому был НЕВЕРЕН.** Зона измерила его сама, опровергла и послала поправку до того, как он доехал до
|
||||
канона.
|
||||
⚠ И само слово «юнит» на этой поверхности не определено: наружу оно не выходило никогда, а `source_chars`
|
||||
меряет тот же заказ ЗНАКАМИ. Либо канон вводит единицу, либо счётчики этого случая переводятся в знаки —
|
||||
это решение оркестратора, не зоны; зона называет цену обоих: во втором случае двигается `unitDone`/
|
||||
`unitTotal` и их пины.
|
||||
|
||||
**`Book`** — добавлены `character_count_exact: boolean` и `structure: "declared"|"detected"|"none"|null`.
|
||||
⚠ Семантика `character_count: null` НЕ ТРОНУТА («книга ещё грузится») — форма признака ратифицирована
|
||||
|
|
@ -405,7 +850,103 @@ blocked: Blocked | null
|
|||
|
||||
**Строка 301** (`a411c10`) — шовная дыра словаря `--max-units`, заведена оркестратором по моему пингу.
|
||||
|
||||
### БАТАРЕЯ — ЧИСЛАМИ И С НАЗВАННЫМИ УСЛОВИЯМИ
|
||||
### БАТАРЕЯ — ФИНАЛ 06.09, СНЯТО ПОСЛЕ ПОСЛЕДНЕЙ ПРАВКИ
|
||||
|
||||
⚠ **Норма, которой этот блок обязан своим существованием, и она куплена ошибкой:** «код написан и пин
|
||||
заведён» — НЕ то же, что «прогнано». За этот пак я дважды объявила починку сделанной, имея код и пин,
|
||||
но не имея прогона; второй раз это стоило оркестратору красноты в `HEAD` (`32bbadb`). С тех пор числа
|
||||
снимаются **после ПОСЛЕДНЕЙ правки, без исключений для комментарных** — четыре прогона (v10, v12 и
|
||||
два ранних) были ОСТАНОВЛЕНЫ и не цитируются, потому что дерево под ними менялось. Наполовину снятые
|
||||
числа опаснее отсутствующих: они выглядят как результат.
|
||||
|
||||
```
|
||||
условия: TM_PLATFORM_TEST_DSN (PostgreSQL 18.4, /tmp:55433; у роли ЕСТЬ CREATEDB) ·
|
||||
TM_PLATFORM_TEST_ENGINE_BIN (tmctl, собран из коммита 0801abc) ·
|
||||
TM_PLATFORM_TEST_BOOK_TEMPLATE (шаблон на СНАПШОТЕ движковых конфигов того же коммита, вне
|
||||
репозитория — рабочее дерево бэкенда правит параллельная сессия) ·
|
||||
TM_PLATFORM_TEST_PGDUMP/PGRESTORE · достижимый пользовательский systemd
|
||||
load average 5.3 перед стартом — режим, в котором время не врёт (D39.197 п.5)
|
||||
|
||||
make check → MAKE-EXIT=0 ⟵ ПЕРЕ-СНЯТО 06.09 ПОСЛЕ ДОФИКСА ПО КАНОНУ, красных НЕТ
|
||||
golangci-lint: 0 issues · gofmt чист · go vet чист · sqlc diff чист
|
||||
ПАКЕТОВ 20: ok 20, FAIL 0
|
||||
ТЕСТОВ верхнеуровневых 855: PASS 850 · FAIL 0 · SKIP 5
|
||||
РЕГИСТР: 458 рядов · открытых 106 · major 3 · minor 35 · info 68
|
||||
⚠ Подметание проверено СКВОЗНО: два посаженных лога мёртвых PID перед прогоном,
|
||||
«ЛОГОВ ПОСЛЕ: 0» — рецепт снял их сам, своего не оставил.
|
||||
(847-й — гейт прибора `TestTheBatteryCannotReportCleanlinessWithoutItsLog`; 848-й —
|
||||
выделенный пин провенанса `TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull`;
|
||||
849-й — пин ЖИЗНЕННОГО состояния `pgstore.TestAParsedBookIsNotStartedBeforeItHasAnyProvenance`,
|
||||
заведённый потому, что прежний доказывал ОТОБРАЖЕНИЕ, а канон говорил про ЖИЗНЬ)
|
||||
⚠ ИСПР.: прежняя редакция этого блока писала «ПАКЕТОВ 21». Их ДВАДЦАТЬ.
|
||||
⛔ И ОБЪЯСНЕНИЕ, которое здесь стояло («двадцать первым я посчитала лишнюю голую
|
||||
строку `FAIL`»), тоже НЕ ВЫДЕРЖАЛО ПРОВЕРКИ — это была третья догадка подряд об одном
|
||||
числе. Пере-считано по сохранённым выводам: зелёный прогон даёт `ok` 20, `FAIL` 0 —
|
||||
лишней строки нет ВОВСЕ; красный даёт `ok` 19 + `FAIL <пакет>` 1 + голых `FAIL` ДВА,
|
||||
итого 22 строки при тех же 20 пакетах. ⇒ «21» получилось СЛОЖЕНИЕМ ДВУХ РАЗНЫХ
|
||||
ПРОГОНОВ: `ok 20` взято из зелёного, `FAIL 1` — из красного. ⚠ **ИСПР. ПОВТОРНО:** здесь стояло «три
|
||||
наблюдателя, три причины ОДНОГО числа». Неверно, и снято по замеру оркестратора: у него
|
||||
`grep -c '^ok.*internal/money'` = **2** в одном логе и 1 после починки — дубль реален и
|
||||
объясняет ЕГО двадцать один. **Чисел было ДВА, а не одно:** его — от дубля в общем логе,
|
||||
моё — от сложения двух прогонов. Общего у них только величина. ⚠ Формулировка была
|
||||
красивее правды и потому прожила дольше неё — ровно то, за чем этот отчёт весь день
|
||||
гоняется.
|
||||
⚠ счёт снят из `.check.log` (`make check` гонит `-v`); `go test` БЕЗ `-v` строк
|
||||
`--- SKIP` не печатает вовсе, и греп по нему даёт ЛОЖНЫЙ НОЛЬ — этой ошибкой
|
||||
четвёртый заход уже был пойман
|
||||
ALARM PD-count: 14 (baseline 14) — ⚠ ЧИСЛО ВЫРОСЛО 06.09 НЕ ОТ НОВОЙ ОПАСНОСТИ, а от того, что две
|
||||
находки пака наконец получили РЯДЫ: `PD-455` и `PD-456` объявлены в `alarmBaseline` с
|
||||
доводом, по прямой инструкции самого гейта. Маркеры они несут законно (холд, деньги,
|
||||
молчание), ниже major стоят потому, что ни одна не теряет денег и не портит работы —
|
||||
ломается продукт. Прежде: 12 (baseline 12), ПЕРЕ-СНЯТА базой: PD-375 и PD-422 удалены из `alarmBaseline`
|
||||
по прямой инструкции самого гейта («ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ») и
|
||||
прецеденту PD-168; было 14 (baseline 14) с двумя объявленными исключениями
|
||||
|
||||
КРАСНЫХ НЕТ. Единственный красный этого пака — `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
|
||||
(«announces 0.12.0, canon is 0.11.0») — ПОГАШЕН оркестратором вторым актом: канон на диске
|
||||
0.12.0, пара сомкнулась. Порядок был ратифицирован — код первым, канон вторым (эррата 04.09-в:
|
||||
обратный порядок стоил суток лжи на проводе), и он сработал как задумано.
|
||||
|
||||
⛔ ДЕФЕКТ САМОГО ПРИБОРА, найденный этим же прогоном и НЕ ПОЧИНЕННЫЙ (передан оркестратору):
|
||||
`make check` пишет лог в ФИКСИРОВАННЫЙ путь `platform/.check.log` и удаляет его при зелени. Два
|
||||
прогона в одном каталоге — а сегодня они шли — молча уничтожают улику друг друга, и провал этот
|
||||
НЕ ГРОМКИЙ: греп по `--- SKIP` не находит файла, и Makefile печатает
|
||||
«--- every test ran: no host condition was missing ---». **Это ЛОЖНАЯ ЧИСТАЯ СПРАВКА:** скипы были
|
||||
(пять), а строка сказала, что не было ни одного. Наблюдено 06.09 в прогоне v16 — три подряд
|
||||
`grep: .check.log: No such file or directory`, и следом чистая справка. ⇒ числа ниже сняты НЕ из
|
||||
`make check`, а собственным `go test ./... -race -count=1 -v` в файл вне каталога зоны, которого
|
||||
чужой прогон не достанет. Починка мелкая (падать громко, если лога нет, либо уникальное имя), но
|
||||
Makefile читает гейт `gates.TestTheBatteryNamesEveryHostConditionItsTestsRead`, и правку прибора
|
||||
посреди чужого лендинга зона в свой дофикс НЕ внесла. ✅ **ПОЧИНЕНО ОТДЕЛЬНЫМ АКТОМ** после закрытия
|
||||
лендинга, по заказу оркестратора — секция «ПРИБОР ПЕРЕСТАЛ УМЕТЬ…» выше, ряд `PD-454`. Числа ниже
|
||||
сняты УЖЕ ПОСЛЕ этой починки, и `make check` в том же прогоне напечатал честное
|
||||
«did NOT run: 5 skipped» вместо прежней чистой справки.
|
||||
|
||||
СКИПОВ ПЯТЬ, ВСЕ НАЗВАНЫ, ВСЕ ОДНОЙ ПРИЧИНЫ — артефакта контраста (`mining-contrast.zh.txt`) нет на
|
||||
этом хосте ни под одним путём: он многомегабайтный, поставляется деплоем и намеренно не в git.
|
||||
Все пять — ПИШУЩИЕ живые пробы, и условие честно их:
|
||||
TestTheRealEngineNamesItsRestorePointInTheLineThisPlatformParses
|
||||
TestALivePreviewWritesNothingAndALiveApplyWrites
|
||||
TestALiveBuildOfAHollowBookWritesTheMarkedCopyInsteadOfRefusing
|
||||
TestWithoutPartialTheSameBookIsRefusedWithTheBuildsOwnNumber
|
||||
TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem
|
||||
|
||||
док-гейты → counts.py --lint: в зоне `platform/docs/**` красных якорей **0**. ⚠ Двенадцать якорей
|
||||
зоны уехали от правок пятого захода и ПЕРЕ-НАЦЕЛЕНЫ — каждый с проверкой, что
|
||||
ожидаемый токен в новой строке ЕДИНСТВЕННЫЙ. Остались 2 красных в `docs/PROGRESS.md`,
|
||||
целящие в канон (`openapi.yaml:2070`, `:2246`) — обе цели уехали от правок канона
|
||||
ОРКЕСТРАТОРОМ, зона чужого журнала не трогает; сказано ему.
|
||||
→ counts.py --check: **РАСХОЖДЕНИЯ, 2, и они ЖДУТ ОРКЕСТРАТОРА.** `docs/PROGRESS.md`
|
||||
несёт «открытых рядов регистра платформы — 107 (major 5)»; пере-счёт даёт **102** и
|
||||
**major 3**. Причина названа: пять рядов помечены `fixed(628cc56)` — каждый своим же
|
||||
текстом говорил «статус флипает ЛЕНДИНГ», лендинг состоялся, и держать их открытыми
|
||||
значило бы врать следующей смене о том, что осталось сделать. Регистр — моя зона,
|
||||
журнал — его.
|
||||
миграция → 00033: up → down → up на отдельной базе, чисто, в одной транзакции (пере-снято после
|
||||
двух последних правок схемы — четвёртый заход поймал, что этого не делали)
|
||||
```
|
||||
|
||||
### БАТАРЕЯ — ЧИСЛАМИ И С НАЗВАННЫМИ УСЛОВИЯМИ (ПРЕЖНИЙ ЗАХОД, 05.09 — ИСТОРИЯ)
|
||||
|
||||
⚠⚠ **ПОПРАВКА К САМОЙ СЕБЕ, И ОНА ТОГО ЖЕ КЛАССА, ЧТО ЛОВИТ КАНОН: моё «скипов 0» было снято
|
||||
командой, которая скипы НЕ ПЕЧАТАЕТ.** Я мерила `go test ./... -count=1` без `-v` и грепала вывод на
|
||||
|
|
@ -418,6 +959,10 @@ blocked: Blocked | null
|
|||
теста, и вердикт обоих прогонов НЕОТЛИЧИМ по коду возврата.
|
||||
|
||||
```
|
||||
⛔⛔ ЧИСЛА НИЖЕ — ПРЕЖНИЕ (v4, 05.09) И СОХРАНЕНЫ КАК ИСТОРИЯ ЗАХОДА. ДЕЙСТВУЮЩИЕ — В БЛОКЕ «ФИНАЛ
|
||||
06.09» СРАЗУ ЗА ЭТИМ. Прежние сняты до лендинга канона и до пятого захода: тогда гейт версии был
|
||||
красен по истинной причине, скипов было пять при других именах, а ALARM стоял на 14.
|
||||
|
||||
СНЯТО ПОСЛЕДНИМ ДЕЙСТВИЕМ, на ТИХОЙ машине (load average 1.07 перед стартом; два предыдущих прогона
|
||||
под load 12–19 упирались в 600-с таймаут пакетов pgstore/runs — правило D39.197 п.5, красный по
|
||||
времени под нагрузкой не результат).
|
||||
|
|
@ -430,8 +975,14 @@ blocked: Blocked | null
|
|||
|
||||
make check → MAKE-EXIT=2
|
||||
golangci-lint 2.12.2: 0 issues · gofmt чист · go vet чист
|
||||
sqlc diff чист (миграция 00033 пере-генерации не потребовала: новые колонки в
|
||||
таблицах, для которых sqlc не держит запросов)
|
||||
sqlc diff чист. ⚠ ИСПР.: первая редакция объясняла это тем, что новые колонки лежат
|
||||
«в таблицах, для которых sqlc не держит запросов» — ЭТО НЕВЕРНО и было догадкой, а
|
||||
не проверкой: `queries/credits.sql` и `queries/exports.sql` читают `books`,
|
||||
`chapters` и `runs`. Верная причина: НИ ОДИН из этих запросов не выбирает `*` и не
|
||||
называет новых колонок, поэтому сгенерированный код не двигается (проверено грепом
|
||||
по `*.sql.go` — вхождений нуль). ⚠ И следствие, которое ложная причина скрывала:
|
||||
`books` ЛЕЖИТ в мире sqlc, значит риск с NULLABLE денежными колонками (находка
|
||||
ревью) — не теоретический, а отложенный до первого запроса, который их выберет.
|
||||
19 пакетов ok
|
||||
go test ./... -RACE -count=1 -v: RUN 845 · PASS 839 · SKIP 5 · FAIL 1
|
||||
(пере-снято ПОСЛЕ дофикса по двум линзам приёмки — числа выше него недействительны)
|
||||
|
|
@ -553,10 +1104,40 @@ status` на входе сессии — clean). В него ВХОДИТ `81a89
|
|||
«ДА» — исправлено. `Resume` для `paused` по-прежнему отвечает `ErrCeilingReached`, а его
|
||||
комментарий обещает ремедиа «новый прогон с бо́льшим потолком»: форму, которую провод после этого
|
||||
минора выразить не умеет — потолка в главах больше нет.
|
||||
- **Стоимость чтения формы заказа выросла и НЕ ЗАМЕРЕНА.** Отчёт празднует снятие квадратичности в
|
||||
`Affordable` на этом же пути — честно тогда сказать и про добавленное: остаток книги теперь
|
||||
агрегируется ПО ЮНИТАМ с коррелированным `not exists` на каждый, вместо одной строки на главу. На
|
||||
книге в 2283 главы это тысячи проб на каждый поллинг. План я не снимала.
|
||||
- ⛔ **ВОПРОС ВЛАДЕЛЬЦУ О ПРОДУКТЕ, НЕ ЗАКРЫТЫЙ И НАМЕРЕННО НЕ ЗАКРЫТЫЙ МОЛЧА: число ЗНАКОВ, которое
|
||||
человек ввёл, не хранится нигде.** `resolveOrder` разрешает `*in.Characters` в `n` юнитов через
|
||||
`pricing.UnitsFor` и само число выбрасывает: колонки нет, поля нет. После перезагрузки экран
|
||||
ответит «5 фрагментов» там, где вопрос был «4500 знаков». ⚠ **Это НЕ то же, что чинит третий
|
||||
вариант, и важно не спутать:** `ordered_units` — единица, в которой мы ОТВЕЧАЕМ; знаки — единица, в
|
||||
которой человек ЗАКАЗЫВАЛ. Публикация `ordered_units` делает несовпадение ВИДИМЫМ, а не создаёт его
|
||||
— то есть это довод ЗА эхо, а не против. Нужно ли эхо вообще — продуктовое решение владельца, не
|
||||
зоны. Цена, если да: одна колонка `runs.ordered_characters` и поле на проводе; ничьей арифметики это
|
||||
не трогает. Найдено ревью коллеги-Fable 06.09, проверено зоной по коду. **В состав минора НЕ
|
||||
внесено** — по слову оркестратора.
|
||||
- ✅ **Стоимость чтения формы заказа — ЗАМЕРЕНА 06.09, И ЗАМЕР ОПРОВЕРГ МОЁ ЖЕ ОПАСЕНИЕ.** Здесь стояло:
|
||||
«остаток агрегируется ПО ЮНИТАМ с коррелированным `not exists` на каждый… на книге в 2283 главы это
|
||||
**тысячи проб на каждый поллинг**. План я не снимала». Механизм назван НЕВЕРНО.
|
||||
|
||||
Снято `EXPLAIN (ANALYZE, BUFFERS)` на книге **2000 глав × 3 юнита** (6000 юнитов, четверть уже
|
||||
доставлена, после `analyze`), одноразовой пробой, удалённой в том же заходе:
|
||||
```
|
||||
Planning Time 1.061 ms · Execution Time 9.585 ms · Buffers: shared hit=143, нулевое чтение с диска
|
||||
Sort → HashAggregate (2000 групп) → Hash Anti Join (6000 × 1500)
|
||||
ReadBookForOrder целиком (два запроса + сканы): best of 5 = 11.3 ms
|
||||
```
|
||||
Планировщик **переписывает коррелированный `not exists` в HASH ANTI JOIN** — один проход по обеим
|
||||
сторонам, а не проба на юнит. «Тысяч проб» не существует ни при каком размере книги; растёт это
|
||||
линейно, а не как произведение.
|
||||
|
||||
⚠ **ГРАНИЦА ЗАМЕРА, и без неё цитировать нельзя:** в таблицах пробы лежала ОДНА книга, поэтому
|
||||
нижние узлы — `Seq Scan` по `units` и `chapters`. На заполненном деплое их место займёт индексный
|
||||
скан по `book_id`, то есть настоящая цена, скорее всего, НИЖЕ измеренной, но форма плана может
|
||||
оказаться другой. Замерено на пустом соседстве; на боевом — нет.
|
||||
|
||||
⚠ И почему это стоит записать отдельно: моё опасение было СВЯЗНЫМ (коррелированный подзапрос ⇒
|
||||
проба на строку) и потому читалось как знание. Прибор ответил иначе за десять минут работы. Это
|
||||
третий случай за пак, когда рассуждение проиграло исполнению, и единственный, где я успела снять
|
||||
замер до того, как утверждение кто-то процитировал.
|
||||
- **Из второго круга НЕ починено и названо поимённо:** ⑴ sqlc-подстановка `*.*_micro_usd → MicroUSD`
|
||||
не различает NULLABLE, а миграция намеренно делает три денежные колонки `books` nullable — сегодня
|
||||
это ничего не ломает (рукописный код читает их как `*int64` и конвертирует сам, `sqlc diff` чист),
|
||||
|
|
@ -564,11 +1145,22 @@ status` на входе сессии — clean). В него ВХОДИТ `81a89
|
|||
`books.source_chars` — nullable `bigint`, для которого подстановки нет, тогда как конфиг обещает
|
||||
указатель для nullable-целых; ⑶ новые фикстуры пишут колонки `pgstore` рукописным SQL ИЗ пакета
|
||||
`internal/runs`, куда не достаёт ни `sqlgate` (читает свой каталог), ни `sqlcgate` — то есть эти
|
||||
вставки не судит никто; ⑷ материализатор манифеста (`readmodel.refresh`) — единственный шов между
|
||||
двумя протестированными половинами — по-прежнему без собственного теста; ⑸ форма над книгой с ЖИВЫМ
|
||||
вставки не судит никто; ⑷ ✅ **ЗАКРЫТО 06.09 — и закрывать пришлось потому, что шов СЛОМАЛСЯ, ровно
|
||||
как эта строка предупреждала.** Материализатор манифеста (`readmodel.refreshStructure`) был
|
||||
единственным швом между двумя протестированными половинами без собственного теста; баг второго
|
||||
носителя `source_chars` завёлся именно в нём и вышел наружу только батареей. Пиньнут двумя тестами
|
||||
без базы (`internal/readmodel`, `fakeStore`): все три денежных члена доезжают в `pgstore.Projection`,
|
||||
счёт знаков едет НА РАЗРЕЗЕ и переживает цену, которую сборка не смогла прочесть. Пины проверены
|
||||
мутациями (`SourceChars: 0` красит оба, потеря `BookOnce` — первый); ⑸ форма над книгой с ЖИВЫМ
|
||||
прогоном отвечает `covers_all` и молчит о том, что клик получит `run_in_flight`; ⑹ глава
|
||||
ЗАДРАФЧЕННАЯ, но не отредактированная, оценивается полностью (счётчики стадию не различают),
|
||||
ошибка ВВЕРХ; ⑺ откат миграции стирает различение двух причин паузы, ради которого пак их и
|
||||
ошибка ВВЕРХ ⟵ ⑵/⑸/⑹ **ПОЛУЧИЛИ РЯДЫ 06.09** (`PD-457`/`PD-455`/`PD-456`): до этого их
|
||||
единственным носителем был ЭТОТ отчёт, а отчёт — хроника, и находка без ряда не находится.
|
||||
⚠ **⑶ РЯДА НЕ ПОЛУЧИЛ И НЕ ДОЛЖЕН, но назван здесь строкой по просьбе оркестратора:** новые фикстуры
|
||||
пишут колонки `pgstore` рукописным SQL ИЗ пакета `internal/runs`, куда не достаёт ни `sqlgate`
|
||||
(читает свой каталог), ни `sqlcgate`. Предмет — ДЫРА В ПОКРЫТИИ ГЕЙТОВ, а не дефект поведения:
|
||||
сегодня ни один сторож не судит эти вставки, и через месяц об этом никто не вспомнит. Кандидат в
|
||||
расширение области `sqlgate`, не в ряд регистра; ⑺ откат миграции стирает различение двух причин паузы, ради которого пак их и
|
||||
разделил — для отката приемлемо, но записано.
|
||||
- **PLAUSIBLE, не проверено исполнением:** что `--resnapshot` на каждом продолжении не приводит к
|
||||
заметной пере-оплате на РЕАЛЬНОЙ книге с растущим банком. Механизм я разобрала по коду движка и
|
||||
|
|
@ -1027,11 +1619,21 @@ owes that and when». Это стало неверным: `tmplatformctl token i
|
|||
собственный отчёт о сломанных якорях краснел бы как сломанный якорь. Читать как «файл, строка,
|
||||
искомый токен → новая строка».
|
||||
|
||||
✅ **Счёт открытых рядов регистра — УЖЕ ПОПРАВЛЕН ТОБОЙ, проверено:** `docs/PROGRESS.md` несёт
|
||||
«открытых рядов регистра платформы — 107 (major 5), всего рядов 453», пере-счёт
|
||||
`python3 docs/scripts/counts.py --check` даёт ровно это и печатает «Литералы сходятся с пере-счётом
|
||||
(7 проверок)». Я завёл ПЯТЬ рядов (`PD-449`…`PD-453`); если между этим отчётом и лендингом появятся
|
||||
ещё чьи-то, число надо пере-снять той же командой.
|
||||
⛔ **СЧЁТ ОТКРЫТЫХ РЯДОВ РЕГИСТРА РАЗЪЕХАЛСЯ ПОСЛЕ ЛЕНДИНГА, И ПОПРАВИТЬ ЕГО МОЖЕШЬ ТОЛЬКО ТЫ.**
|
||||
`docs/PROGRESS.md` несёт «открытых рядов регистра платформы — 107 (major 5), всего рядов 453».
|
||||
Это было верно до 06.09. Пять рядов, чьё лечение лендинг внёс в дерево, помечены `fixed(628cc56)`
|
||||
ЭТОЙ правкой — `PD-375`, `PD-410`, `PD-422`, `PD-440`, `PD-446`; каждый из них своим же текстом
|
||||
говорил «статус флипает ЛЕНДИНГ», лендинг состоялся, и держать их открытыми значило бы врать
|
||||
следующей смене о том, что осталось сделать. ⇒ `python3 docs/scripts/counts.py --check` теперь
|
||||
печатает **РАСХОЖДЕНИЯ** по двум литералам `docs/PROGRESS.md`: пере-счёт даёт **102 открытых**
|
||||
и **major 3** (было 107 и 5; из ушедших major-ами были `PD-410` и `PD-440`), всего рядов 453.
|
||||
Своей рукой чужой журнал не правлю. ⚠ Числа надо пере-снять ТОЙ ЖЕ командой в момент правки:
|
||||
между этим отчётом и ею могут появиться чужие ряды.
|
||||
|
||||
⚠ **Той же правкой `PD-375` и `PD-422` УДАЛЕНЫ из `alarmBaseline`** (`internal/gates/register_test.go`)
|
||||
— по прямой инструкции самого гейта («ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ, и тогда их ids отсюда
|
||||
УДАЛЯЮТСЯ той же правкой») и по прецеденту `PD-168`, записанному в его шапке. Исключение, пережившее
|
||||
свою причину, — это то, как база перестаёт читаться. Гейт зелёный: `go test ./internal/gates/` ok.
|
||||
|
||||
**Три якоря в ТВОИХ файлах уехали от моих правок. Цели проверены, новые строки:**
|
||||
1. `docs/PROGRESS.md`, строка 10 — цель `platform/internal/config/config.go`, строка **293**, токен
|
||||
|
|
@ -3535,7 +4137,7 @@ $0.005460). Накладные масштабируются КНИГОЙ, а н
|
|||
прогон и CLI печатал «холд возвращён целиком». Теперь цикл по всем, `settled_at` — только когда
|
||||
не осталось ни одного; гонку со свипом (`ErrNoReservation`) терпит, как терпит живая ветка.
|
||||
⚠ «Чужой холд» при этом невозможен ПО ПОСТРОЕНИЮ: всё ключуется `runID#attemptNo`
|
||||
(`platform/internal/pgstore/runs.go:237`=`fmt.Sprintf("%s#%d", runID, attempt)`) и идёт через
|
||||
(`platform/internal/pgstore/runs.go:243`=`fmt.Sprintf("%s#%d", runID, attempt)`) и идёт через
|
||||
`closeReservation`+`releaseHold` (символы в `platform/internal/pgstore/credits.go`), которые
|
||||
сверяют владельца.
|
||||
5. **Дефект, который пак внёс в ЧУЖОЙ гейт и который поймала собственная мутационная обвязка:**
|
||||
|
|
@ -3780,7 +4382,7 @@ caught:` того теста, который она обязана валить
|
|||
вне идемпотентности ключа запроса); повтор после успеха отвечает `run_in_flight` либо
|
||||
`ErrRePassUnavailable` — факт погашен финишем (символ `ErrRePassUnavailable`:
|
||||
`platform/internal/runs/runs.go:149`=`var ErrRePassUnavailable = errors.New(`; ветка провода —
|
||||
`platform/internal/httpapi/v0.go:1135`=`case errors.Is(err, runs.ErrRePassUnavailable):`).
|
||||
`platform/internal/httpapi/v0.go:1158`=`case errors.Is(err, runs.ErrRePassUnavailable):`).
|
||||
Вырожденных дублей не нашёл, но специального пина нет.
|
||||
- ⚠ **Для оркестратора — находка опровергателя P10, носителя ни в регистре, ни в бэклоге у неё нет:**
|
||||
якоря §2.12 компаньона контракта (`docs/architecture/14-api-contract/README.md`, греп
|
||||
|
|
@ -4165,7 +4767,7 @@ mutation-catch (спутать пары «числитель×база»); ко
|
|||
**⛔ ПИНГ ОРКЕСТРАТОРА №19 — 23.08, ЗОНЕ ПЛАТФОРМЫ.** ⚠ Его собственная шапка звала находки «тремя», а нумеровала ЧЕТЫРЕ — считать по нумерации. Живых из четырёх ДВЕ: находка 1 ниже дословно, находка 4 — строкой регистра. Две закрыты паком P9 и проверяются грепом, а не памятью: блокер провайдерских ключей (строка **211** единого бэклога) — ключи едут аргументом `--keys-file`, греп `TM_PLATFORM_ENGINE_KEYS_PATH`; дубль движковой конвенции пути (строка **213**) — `runner.projectDB` снесён, путь берётся из `artifacts.bank_export` манифеста. Находка 4 (мёртвое поле `ingest.StatusReport.UnsignedBankTerms`) жива и несёт свой якорь строкой `PD-396` (греп `UnsignedBankTerms` в регистре и в `platform/internal/ingest/resync.go`).
|
||||
|
||||
1. **Пере-нарезка книги сносит ВСЕ решения юнитов и пересчитывает прогресс из пустоты.**
|
||||
`internal/pgstore/readmodel.go:144`=`a re-cut drops the resolutions of the previous cut` —
|
||||
`internal/pgstore/readmodel.go:177`=`a re-cut drops the resolutions of the previous cut` —
|
||||
это не догадка, там собственный комментарий кода. Строку в ваш регистр я не заводил: решать вам,
|
||||
намеренная это семантика пере-разреза или дефект, и что делать со счётчиками, которые после
|
||||
этого показывают ноль сделанного на книге, где работа была.
|
||||
|
|
|
|||
|
|
@ -134,57 +134,10 @@ func TestTheBatteryNamesEveryHostConditionItsTestsRead(t *testing.T) {
|
|||
// vanishes from the hint and BOTH sides fall silent together — the walk would not see it either if
|
||||
// the walk were narrowed to match. So the recipe's own anchors are read here and held to the ones
|
||||
// this gate understands; narrowing either is what goes red, before any test uses the idiom.
|
||||
recipe, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The recipe is cut the way MAKE cuts it — the target line, then every line that is blank or
|
||||
// tab-indented — and not at the first blank line: a recipe split into two indented lines with a
|
||||
// blank between them is legal make with identical output, and a gate that failed on it would send
|
||||
// the next person who tidies this file looking for a defect that is not there.
|
||||
lines := strings.Split(string(recipe), "\n")
|
||||
start := -1
|
||||
for i, l := range lines {
|
||||
if strings.HasPrefix(l, "conditions:") {
|
||||
start = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
t.Fatal("the Makefile declares no `conditions` target: the battery's hint has no source")
|
||||
}
|
||||
var recipeLines []string
|
||||
for _, l := range lines[start+1:] {
|
||||
if l != "" && !strings.HasPrefix(l, "\t") {
|
||||
break
|
||||
}
|
||||
recipeLines = append(recipeLines, l)
|
||||
}
|
||||
// The GREP EXPRESSIONS the recipe runs, not words that appear near them: an idiom named in a
|
||||
// comment inside the recipe would satisfy a substring test while the grep beside it had been
|
||||
// narrowed back, which is the hole this check exists to close.
|
||||
//
|
||||
// LOGICAL commands, joined the way the shell joins them: a pipeline split across a backslash
|
||||
// continuation is one command however many lines it occupies, and a check that judged physical
|
||||
// lines would call a legal reformat a defect — the thing the slice above is careful not to do.
|
||||
commands := []string{}
|
||||
pending := ""
|
||||
for _, l := range recipeLines {
|
||||
code := strings.TrimPrefix(l, "\t")
|
||||
if head, _, found := strings.Cut(code, "#"); found {
|
||||
code = head
|
||||
}
|
||||
trimmed := strings.TrimRight(code, " \t")
|
||||
if strings.HasSuffix(trimmed, `\`) {
|
||||
pending += strings.TrimSuffix(trimmed, `\`) + " "
|
||||
continue
|
||||
}
|
||||
commands = append(commands, pending+code)
|
||||
pending = ""
|
||||
}
|
||||
if pending != "" {
|
||||
commands = append(commands, pending)
|
||||
}
|
||||
commands := makeRecipe(t, "conditions")
|
||||
// EVERY grep that looks for an environment condition must carry BOTH idioms, and the recipe runs
|
||||
// more than one of them: the names come from one grep and the packages that read each name from
|
||||
// another. An idiom dropped from either is a condition or an attribution that quietly goes
|
||||
|
|
@ -359,3 +312,287 @@ func equal(got, want []string) bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// makeRecipe is the Makefile's recipe for one target, as the SHELL would see it: one string per
|
||||
// logical command, comments cut, backslash continuations joined.
|
||||
//
|
||||
// It lives in one place because two gates read recipes now, and two hand-written parsers of one
|
||||
// grammar are a second carrier free to drift from the first — the defect this zone spent a day
|
||||
// removing from its own money path.
|
||||
//
|
||||
// ⚠ The recipe is cut the way MAKE cuts it — the target line, then every line that is blank or
|
||||
// tab-indented — and NOT at the first blank line: a recipe split into two indented lines with a blank
|
||||
// between them is legal make with identical output, and a gate that failed on it would send the next
|
||||
// person who tidies this file looking for a defect that is not there. ⚠ And the joining is what makes
|
||||
// the unit LOGICAL: a pipeline split across a continuation is one command however many lines it
|
||||
// occupies, so a legal reformat is not a finding.
|
||||
func makeRecipe(t *testing.T, target string) []string {
|
||||
t.Helper()
|
||||
recipe, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := strings.Split(string(recipe), "\n")
|
||||
start := -1
|
||||
for i, l := range lines {
|
||||
if strings.HasPrefix(l, target+":") {
|
||||
start = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
t.Fatalf("the Makefile declares no `%s` target: the gate that reads it has no source", target)
|
||||
}
|
||||
var recipeLines []string
|
||||
for _, l := range lines[start+1:] {
|
||||
if l != "" && !strings.HasPrefix(l, "\t") {
|
||||
break
|
||||
}
|
||||
recipeLines = append(recipeLines, l)
|
||||
}
|
||||
commands := []string{}
|
||||
pending := ""
|
||||
for _, l := range recipeLines {
|
||||
code := strings.TrimPrefix(l, "\t")
|
||||
if head, _, found := strings.Cut(code, "#"); found {
|
||||
code = head
|
||||
}
|
||||
trimmed := strings.TrimRight(code, " \t")
|
||||
if strings.HasSuffix(trimmed, `\`) {
|
||||
pending += strings.TrimSuffix(trimmed, `\`) + " "
|
||||
continue
|
||||
}
|
||||
commands = append(commands, pending+code)
|
||||
pending = ""
|
||||
}
|
||||
if pending != "" {
|
||||
commands = append(commands, pending)
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
// ⛔ THE BATTERY MUST NOT BE ABLE TO REPORT CLEANLINESS OVER A LOG IT DID NOT READ.
|
||||
//
|
||||
// Every line `make check` prints about the run — the package list, the ALARM rows, the failure list,
|
||||
// the skip count — is a GREP over one file. Grep answers a MISSING file exactly as it answers a clean
|
||||
// one: with nothing. So the recipe could reach its final `else` and print «every test ran: no host
|
||||
// condition was missing» about a run whose log had vanished, which is a bill of health for a
|
||||
// measurement that never happened.
|
||||
//
|
||||
// It is not hypothetical and it is not rare: observed 06.09 on a run with FIVE skips, three
|
||||
// `grep: .check.log: No such file or directory` followed by that very line. The cause was a FIXED log
|
||||
// name shared by every run in the directory — and two batteries in one directory is the normal case
|
||||
// here, the zone and the orchestrator both run one.
|
||||
//
|
||||
// So the recipe must TEST the log before it reads it and leave RED when it is not there. This gate
|
||||
// holds ONE SAFE SHAPE and says so plainly rather than pretending to prove the property: a test of the
|
||||
// log, then an exit with a LITERAL NON-ZERO code, then the reads. Within that shape the details are
|
||||
// free — the log may be named per-run or not, the guard may be `-s`, `-f` or `-e`. A different but
|
||||
// equally correct shape (say, all the reads nested inside `if [ -s log ]; then … else exit 1; fi`)
|
||||
// will go red here, and that is deliberate: re-shaping the one recipe the whole battery reports
|
||||
// through should be a decision somebody takes and re-pins, not a silent pass.
|
||||
//
|
||||
// Mutation caught: deleting the guard; moving it after the first grep; dropping the `exit` so the
|
||||
// recipe notices and carries on; and — the one the first edition missed — `exit 0`, which satisfies
|
||||
// "there is an exit" while returning SUCCESS from a run whose evidence is gone.
|
||||
func TestTheBatteryCannotReportCleanlinessWithoutItsLog(t *testing.T) {
|
||||
steps := []string{}
|
||||
for _, cmd := range makeRecipe(t, "check") {
|
||||
for _, s := range strings.Split(cmd, ";") {
|
||||
steps = append(steps, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
// WHERE THE LOG COMES FROM, read out of the recipe rather than assumed: the redirect of the test
|
||||
// run names it, so a rename cannot make this gate look at the wrong file and pass.
|
||||
logRef := ""
|
||||
for _, s := range steps {
|
||||
if !strings.Contains(s, "test ./...") {
|
||||
continue
|
||||
}
|
||||
if _, after, found := strings.Cut(s, "> "); found {
|
||||
logRef = strings.Trim(strings.Fields(after)[0], `"`)
|
||||
}
|
||||
}
|
||||
if logRef == "" {
|
||||
t.Fatal("the `check` recipe does not redirect its test run to a file this gate can find: " +
|
||||
"either the battery stopped keeping a log, or its shape moved and this gate now reads nothing")
|
||||
}
|
||||
// One shell command per step, with the leading keyword taken off, so that `then exit 1` is read as
|
||||
// `exit 1` and `then echo …` as an echo. Done once here because every check below asks the same
|
||||
// question of the same string, and two strippers would drift.
|
||||
cmds := make([]string, len(steps))
|
||||
for i, s := range steps {
|
||||
cmd := strings.TrimSpace(s)
|
||||
for {
|
||||
stripped := cmd
|
||||
for _, kw := range []string{"then", "else", "do", "{"} {
|
||||
if rest, found := strings.CutPrefix(stripped, kw); found && strings.TrimLeft(rest, " \t") != rest {
|
||||
stripped = strings.TrimLeft(rest, " \t")
|
||||
}
|
||||
}
|
||||
if stripped == cmd {
|
||||
break
|
||||
}
|
||||
cmd = stripped
|
||||
}
|
||||
cmds[i] = cmd
|
||||
}
|
||||
reads, guard, exits := -1, -1, -1
|
||||
exitCode := ""
|
||||
for i, cmd := range cmds {
|
||||
if !strings.Contains(cmd, logRef) {
|
||||
continue
|
||||
}
|
||||
// A grep INVOCATION, not the word: the recipe's own guard EXPLAINS itself in an `echo`, and the
|
||||
// explanation says «grep». The first edition of this gate counted that sentence as a read and
|
||||
// reported the guard as too late — which is the same substring-versus-invocation trap the
|
||||
// sibling check above spends a paragraph on, walked into by the test that quotes it.
|
||||
greps := strings.Contains(cmd, "$$(grep") ||
|
||||
(!strings.HasPrefix(cmd, "echo") && strings.Contains(cmd, "grep"))
|
||||
switch {
|
||||
case greps && reads < 0:
|
||||
reads = i
|
||||
case strings.HasPrefix(cmd, "if [") || strings.HasPrefix(cmd, "[ "):
|
||||
if guard < 0 && (strings.Contains(cmd, "-s ") || strings.Contains(cmd, "-f ") || strings.Contains(cmd, "-e ")) {
|
||||
guard = i
|
||||
}
|
||||
}
|
||||
}
|
||||
// ⛔ THE EXIT'S CODE, not merely its presence — and this half was MISSING from the first edition.
|
||||
// `exit 0` after the guard satisfied "there is an exit" while leaving the recipe to announce that
|
||||
// the evidence is gone and then RETURN SUCCESS. That is the very defect this gate exists for,
|
||||
// wearing different clothes and worse in one respect: the old false clean bill was a LINE a person
|
||||
// could catch, this one is the EXIT CODE, which is what CI and the landing read. Measured, not
|
||||
// argued: with `exit 1` mutated to `exit 0` this gate stayed green and `make check` returned 0
|
||||
// while printing «THE BATTERY LEFT NO LOG».
|
||||
//
|
||||
// A literal is required. `exit` bare carries the previous command's status — here the status of an
|
||||
// `echo`, which is zero — and `exit $var` cannot be judged from the recipe at all; both are refused
|
||||
// rather than guessed, because a guard whose code is not readable here is a guard this gate cannot
|
||||
// promise anything about.
|
||||
for i, cmd := range cmds {
|
||||
if guard < 0 || i <= guard {
|
||||
continue
|
||||
}
|
||||
if rest, found := strings.CutPrefix(cmd, "exit"); found {
|
||||
exits, exitCode = i, strings.TrimSpace(rest)
|
||||
break
|
||||
}
|
||||
}
|
||||
if reads < 0 {
|
||||
t.Fatalf("no step of the `check` recipe greps %s: the gate is reading the wrong target or the "+
|
||||
"battery no longer reports what the log holds", logRef)
|
||||
}
|
||||
if guard < 0 {
|
||||
t.Fatalf("the `check` recipe reads %s at step %d and never TESTS that it is there. A missing log "+
|
||||
"is indistinguishable from a clean one to grep, so the recipe will print «every test ran» "+
|
||||
"over a run it did not measure — the defect this gate exists for", logRef, reads)
|
||||
}
|
||||
if guard > reads {
|
||||
t.Errorf("the `check` recipe tests %s at step %d but has already read it at step %d: the first "+
|
||||
"read is where the false clean bill starts, so the guard must come before it", logRef, guard, reads)
|
||||
}
|
||||
if exits < 0 || exits > reads {
|
||||
t.Errorf("the `check` recipe tests %s at step %d but does not exit before its first read at step "+
|
||||
"%d: noticing the absence and carrying on prints the same bill of health as never looking",
|
||||
logRef, guard, reads)
|
||||
return
|
||||
}
|
||||
code, err := strconv.Atoi(exitCode)
|
||||
if err != nil {
|
||||
t.Errorf("the guard of the `check` recipe exits with %q, which this gate cannot read as a number: "+
|
||||
"a bare `exit` carries the previous command's status — an `echo`, so zero — and a variable "+
|
||||
"cannot be judged from the recipe. Write a literal, so that what the recipe promises is what "+
|
||||
"a reader can check", "exit "+exitCode)
|
||||
return
|
||||
}
|
||||
if code == 0 {
|
||||
t.Errorf("the guard of the `check` recipe says the log is missing and then exits %d, which is "+
|
||||
"SUCCESS: the battery announces that its evidence is gone and reports a green run. That is "+
|
||||
"the same false clean bill this gate exists for, moved from a LINE a person reads into the "+
|
||||
"EXIT CODE a machine reads — and the landing reads the code", code)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ THE BATTERY MAY DELETE ITS OWN LOG AND NOBODY ELSE'S.
|
||||
//
|
||||
// The per-run name that removed the collision took a property with it: the fixed name limited itself
|
||||
// to ONE stale file, and per-run names accumulate one per failed or interrupted run. The recipe
|
||||
// sweeps them — and a sweep is exactly where the original defect can walk back in, because
|
||||
// `rm -f .check.log.*` is the obvious way to write it and it deletes a CONCURRENT run's evidence
|
||||
// mid-recipe, which is the collision the per-run name exists to prevent.
|
||||
//
|
||||
// So the shape held here is: any removal aimed at a log OTHER than this run's own must carry a
|
||||
// liveness test in the same step. `kill -0` is what the recipe uses — POSIX, no `/proc` — and a
|
||||
// recycled PID keeps a stale file, which is the conservative direction and the only one that cannot
|
||||
// destroy evidence.
|
||||
//
|
||||
// Mutation caught: replacing the conditional sweep with a bare `rm -f .check.log.*`; dropping the
|
||||
// liveness test while keeping the loop; sweeping after the run has started rather than before.
|
||||
func TestTheBatterySweepsOnlyLogsWhoseWriterIsGone(t *testing.T) {
|
||||
steps := []string{}
|
||||
for _, cmd := range makeRecipe(t, "check") {
|
||||
for _, s := range strings.Split(cmd, ";") {
|
||||
steps = append(steps, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
logRef := ""
|
||||
for _, s := range steps {
|
||||
if strings.Contains(s, "test ./...") {
|
||||
if _, after, found := strings.Cut(s, "> "); found {
|
||||
logRef = strings.Trim(strings.Fields(after)[0], `"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if logRef == "" {
|
||||
t.Fatal("the `check` recipe does not redirect its test run to a file this gate can find")
|
||||
}
|
||||
// A removal of THIS run's own log is the ordinary cleanup and needs no liveness test: nothing else
|
||||
// can be writing it. Every OTHER removal is aimed at somebody else's file.
|
||||
swept := false
|
||||
for _, s := range steps {
|
||||
if !removesAFile(s) || strings.Contains(s, logRef) {
|
||||
continue
|
||||
}
|
||||
swept = true
|
||||
if !strings.Contains(s, "kill -0") {
|
||||
t.Errorf("the `check` recipe removes a log that is not its own without asking whether its "+
|
||||
"writer is still alive:\n\t%s\nA sweep that does not test liveness deletes a concurrent "+
|
||||
"run's evidence mid-recipe — the collision the per-run name was introduced to remove, "+
|
||||
"walking back in through the tidy-up", s)
|
||||
}
|
||||
}
|
||||
if !swept {
|
||||
t.Error("the `check` recipe never removes an OLD log. Per-run names do not clean up after " +
|
||||
"themselves: every failed or interrupted run leaves its file behind, and the heap becomes " +
|
||||
"indistinguishable from the one log a failing run deliberately keeps for a reader")
|
||||
}
|
||||
}
|
||||
|
||||
// removesAFile says whether a recipe step INVOKES `rm`, as opposed to containing those two letters.
|
||||
//
|
||||
// ⚠ Written as a function after a substring test matched «ala`rm m`arkers» inside an echo and blamed
|
||||
// the recipe for a defect it did not have. That is the THIRD time in one shift that a check of this
|
||||
// file confused a word with a command — the sibling gate warns about it in prose, the guard's own
|
||||
// echo tripped the exit check, and now this. Prose warnings evidently do not stop it; a named
|
||||
// function that every such check must go through has a chance to.
|
||||
func removesAFile(step string) bool {
|
||||
// ⚠ MAKE'S SILENCE PREFIX IS GLUED TO THE FIRST COMMAND OF A RECIPE (`@rm -f …`), so the first
|
||||
// token is `@rm` and not `rm`. Found by a mutation that this gate DID catch — but through the
|
||||
// wrong assertion, reporting «never removes an old log» about a recipe whose first act was
|
||||
// removing them all. A right verdict for a wrong reason is a hole, not a pass.
|
||||
fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(step), "@"))
|
||||
for i, f := range fields {
|
||||
if f != "rm" {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
return true
|
||||
}
|
||||
switch fields[i-1] {
|
||||
case "||", "&&", "then", "else", "do", "{":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,27 +53,39 @@ var alarmMarkers = []string{"деньг", "холд", "молча", "блоки
|
|||
// not see, because on that tree the closure had not happened yet and the session correctly refused
|
||||
// to write a base against a closure that did not exist. It exists now, and the gate says so:
|
||||
// `ALARM PD-168 LEFT the class (status "fixed(6ae3e76)", weight "minor, деньги")`.
|
||||
//
|
||||
// ⚠ `PD-375` and `PD-422` LEFT on 06.09, by the same instruction and the same precedent. They had
|
||||
// entered on 05.09 from the TEXT OF THEIR TREATMENT rather than from any new danger — a disposition
|
||||
// naming money and the hold, because the treatment was about money and the hold — and they were
|
||||
// declared here for exactly as long as the landing had not happened. It has (`fixed(628cc56)`), so
|
||||
// the declaration goes with it: an exception kept past its reason is how a baseline stops being read.
|
||||
// · PD-375 — the carrier (`in.CeilingChapters < bounds.Min`) died with the scale in chapters; the
|
||||
//
|
||||
// class «no test executes the load-bearing half» is closed by
|
||||
// `runs.TestAnOrderTheBalanceCannotCarryIsRefusedBeforeTheHold`.
|
||||
//
|
||||
// · PD-422 — the `--resnapshot` condition widened to `BankMoved || HasPriorRun`; pinned by
|
||||
//
|
||||
// `runs.TestASecondPurchaseCarriesResnapshotEvenWithoutABankCorrection`.
|
||||
var alarmBaseline = []string{
|
||||
// PD-448 — гонка двух резюмов: проигравший получает отказ вместо прогона. Внесён 05.09 той же
|
||||
// правкой, что и ряд. Маркеры класса он несёт законно: ряд говорит о ДЕНЬГАХ (холд берётся
|
||||
// ровно один) и о том, что отказ приходит МОЛЧА для пользователя, дважды нажавшего кнопку.
|
||||
// Ниже major он стоит потому, что деньги не теряются: отказ случается ДО взятия холда.
|
||||
"PD-448",
|
||||
// PD-375 и PD-422 — вошли в класс 05.09 паком «форма заказа», и вошли ОТ ТЕКСТА О ЛЕЧЕНИИ, а не
|
||||
// от новой опасности: обоим рядам дописана диспозиция, называющая деньги и холд, потому что
|
||||
// лечение именно про них. Это ложное срабатывание эвристики маркеров — ровно тот случай, ради
|
||||
// которого у этого гейта есть объявляемое исключение, а не молчание.
|
||||
//
|
||||
// ⚠ ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ, и тогда их ids отсюда УДАЛЯЮТСЯ той же правкой —
|
||||
// прецедент PD-168 в шапке выше. Пока лендинга нет, статус у них честно `open`: писать
|
||||
// `fixed(<sha>)` против закрытия, которого в истории ещё не произошло, эта зона уже отказывалась
|
||||
// делать, и отказывалась правильно.
|
||||
// · PD-375 — носитель дефекта (`in.CeilingChapters < bounds.Min`) умер вместе со шкалой в главах;
|
||||
// класс «несущую половину не исполняет ни один тест» закрыт новым пином
|
||||
// `runs.TestAnOrderTheBalanceCannotCarryIsRefusedBeforeTheHold`.
|
||||
// · PD-422 — условие `--resnapshot` расширено до `BankMoved || HasPriorRun`; пин
|
||||
// `runs.TestASecondPurchaseCarriesResnapshotEvenWithoutABankCorrection`.
|
||||
"PD-375", "PD-422",
|
||||
// PD-455 — вошёл в класс 06.09 вторым кругом пака «форма заказа». Маркеры законны: ряд про то, что
|
||||
// форма МОЛЧИТ о живом прогоне, и про ХОЛД, потому что отказ случается ровно до его взятия. Ниже
|
||||
// major он стоит по той же причине, по которой его цена терпима: денег не теряется и работа не
|
||||
// портится — ломается обещание «вердикт ДО клика», то есть продукт, а не деньги.
|
||||
"PD-455",
|
||||
// PD-456 — вошёл в класс 06.09 вторым кругом пака «форма заказа». Маркеры несёт ЗАКОННО: ряд про
|
||||
// деньги и про холд, потому что дефект — завышение оценки остатка (юнит, у которого сделан только
|
||||
// черновик, входит в цену обеими волнами). Ниже major он стоит потому, что **ошибка направлена
|
||||
// ВВЕРХ**: денег не теряется и работа не портится, у человека просят зарезервировать больше
|
||||
// нужного. Цена продуктовая — завышенный холд сокращает покупаемое, а на коротком балансе делает
|
||||
// покупку невозможной там, где она возможна. Лечение упирается в манифест движка (остаток по
|
||||
// ВОЛНАМ, а не только по юнитам), то есть в чужую зону, и одной платформой не закрывается.
|
||||
"PD-456",
|
||||
"PD-94", "PD-107", "PD-162", "PD-201", "PD-212",
|
||||
"PD-217", "PD-244", "PD-418", "PD-420", "PD-428", "PD-433",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,30 @@ import "net/http"
|
|||
// client generated against another one refuses to work and says so — which is why this must be
|
||||
// raised in the same commit as the code that implements a new minor, and never as a courtesy
|
||||
// afterwards.
|
||||
// ⚠ 0.11.0 is the ORDER FORM's minor (D39.196, unified backlog row 279), and while the canon on disk
|
||||
// still reads 0.10.0 the gate internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified
|
||||
// is RED — by its true cause, which is that the pair is not closed yet. The order was ratified by the
|
||||
// orchestrator on 05.09: the CODE lands first and the canon follows in the second act. The reverse
|
||||
// order was refused deliberately — errata 04.09-в is the day the canon moved first and the wire spent
|
||||
// a working day announcing a version it did not serve.
|
||||
const ContractVersion = "0.11.0"
|
||||
// ⚠ 0.11.0 was the ORDER FORM's minor (D39.196, unified backlog row 279) and it lived for HOURS.
|
||||
// 0.12.0 is the same form's correction: `Run.ordered_chapters` became nullable and `ordered_units`
|
||||
// arrived beside it, because the chapter figure a character order used to publish was the SPAN it
|
||||
// reached into and overstated what was bought by up to a whole chapter — worst on the cheapest order
|
||||
// there is. That changes the wire this build serves, so the number moves with it, in this same
|
||||
// change and not as a courtesy afterwards.
|
||||
//
|
||||
// ⚠ Why a bump and not an amendment inside 0.11.0, decided rather than assumed: 0.11.0's canon text
|
||||
// was edited three times after its landing, and none of those was a change of FORM — an open
|
||||
// vocabulary growing by one value, a defect note, and an ERRATUM correcting what the document said
|
||||
// about members the wire already served. Prose and errata do not move a number; a served shape does.
|
||||
// And 0.11.0 already has its acceptance act (D39.208), which closes it. See the pack's report.
|
||||
//
|
||||
// ⚠ THE GATE IS RED WHILE THE CANON READS 0.11.0, and that is the ratified order and its true cause:
|
||||
// the CODE lands first and the canon follows in the second act. The reverse was refused deliberately
|
||||
// — errata 04.09-в is the day the canon moved first and the wire spent a working day announcing a
|
||||
// version it did not serve.
|
||||
//
|
||||
// ⛔ AND WHAT THE GATE CANNOT SEE, said here because this constant is where a reader comes looking: it
|
||||
// compares VERSIONS, not SHAPES. Three separate untruths in the 0.11.0 text passed it in one day —
|
||||
// the bar described in chapters while the wire sent units, `ordered_chapters: 0` promised where the
|
||||
// wire sent 2, three values of a vocabulary that had four — because the NUMBER matched each time. The
|
||||
// bump keeps the number from lying; it does not close the blind spot, which is unified backlog row 309.
|
||||
const ContractVersion = "0.12.0"
|
||||
|
||||
// Capabilities is what this deployment can do: one flat document, the same for every account.
|
||||
type Capabilities struct {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func TestStopAndResumeAnswerWithTheRunTheContractDescribes(t *testing.T) {
|
|||
{"resume", func(f *fakeRuns) string { return f.resumed }},
|
||||
} {
|
||||
rn := &fakeRuns{run: pgstore.Run{
|
||||
ID: "run_9", Revision: 12, Status: "stopped", VerifyBank: true, OrderedChapters: 100,
|
||||
ID: "run_9", Revision: 12, Status: "stopped", VerifyBank: true, OrderedChapters: ptr(100),
|
||||
StartedAt: time.Unix(0, 0).UTC(),
|
||||
}}
|
||||
w := call(t, v0Server(t, &fakeLibrary{}, rn), "POST", "/v0/runs/run_9/"+tc.path, "")
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ func TestTheClaimCarriesTheRequestsOwnOperation(t *testing.T) {
|
|||
// Mutation caught: claiming the route pattern instead of the request's path.
|
||||
func TestTheClaimOfARunCarriesTheBooksOwnPath(t *testing.T) {
|
||||
keys := newFakeKeys()
|
||||
rn := &fakeRuns{run: pgstore.Run{ID: "run_1", BookID: "bk_1", Status: "translating", OrderedChapters: 10}}
|
||||
rn := &fakeRuns{run: pgstore.Run{ID: "run_1", BookID: "bk_1", Status: "translating", OrderedChapters: ptr(10)}}
|
||||
h := v0ServerWith(t, Deps{Library: &fakeLibrary{}, Runs: rn, Keys: keys})
|
||||
r := httptest.NewRequest("POST", "/v0/books/bk_1/runs",
|
||||
strings.NewReader(`{"stop_for_signing":false,"chapters":10}`))
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ func projectRun(r pgstore.Run) wireRun {
|
|||
// can look at the terms", and the flag is how that reaches the engine.
|
||||
StopForSigning: r.VerifyBank,
|
||||
OrderedChapters: r.OrderedChapters,
|
||||
OrderedUnits: r.OrderedUnits,
|
||||
DeliveredChapters: r.DeliveredChapters,
|
||||
TermConsistencyFunded: r.BondFunded,
|
||||
// The user's own click, which no `status` answers: a stop asked for mid-translation can meet
|
||||
|
|
|
|||
|
|
@ -209,10 +209,19 @@ type wireBook struct {
|
|||
// destroy a meaning the contract already promises. Form ratified by the orchestrator, act
|
||||
// D39.201 §5(б), on two lawful shapes the zone put up.
|
||||
CharacterCountExact bool `json:"character_count_exact"`
|
||||
// Structure is where the chapter boundaries came from: `declared` (the format drew them),
|
||||
// `detected` (matched in the prose), `none` (the whole book is one chapter). Null until a
|
||||
// manifest has said. A client needs it to know what a chapter NUMBER is worth before it offers an
|
||||
// order phrased in one — see OrderOptions.chapter_orders.
|
||||
// Structure is where the chapter boundaries came from, in the ENGINE's own word, passed through
|
||||
// untouched. A client needs it to know what a chapter NUMBER is worth before it offers an order
|
||||
// phrased in one — see OrderOptions.chapter_orders.
|
||||
//
|
||||
// ⚠ THE VOCABULARY IS OPEN AND GROWS ADDITIVELY, so this comment does not list it: an earlier
|
||||
// edition named three words, the engine shipped a fourth (`delimited`, 06.09), and the list went
|
||||
// stale in place while reading like a promise. The canon holds the words; this holds the shape.
|
||||
//
|
||||
// ⛔ NULL IS NOT A WORD OF THAT VOCABULARY AND NOT `none`. `none` is a book the engine LOOKED at
|
||||
// and found one chapter in — a statement about the book. Null is the absence of any statement:
|
||||
// no manifest has been read. ⚠ That is NOT the same as «still arriving»: a book can be fully here
|
||||
// and still answer null, because the cut has not been run. Pinned by
|
||||
// TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull, whose book is `not_started`.
|
||||
Structure *string `json:"structure"`
|
||||
AddedAt time.Time `json:"added_at"`
|
||||
NoteCount int `json:"note_count"`
|
||||
|
|
@ -231,13 +240,27 @@ type wireRun struct {
|
|||
Status string `json:"status"`
|
||||
StopForSigning bool `json:"stop_for_signing"`
|
||||
StopRequested bool `json:"stop_requested"`
|
||||
// OrderedChapters and DeliveredChapters are what the run bought and what it has handed over.
|
||||
// `ordered_chapters` is what `ceiling_chapters` was called until 0.11.0, renamed because the word
|
||||
// «ceiling» belonged to money and this number never was money — it is how much BOOK was sold.
|
||||
OrderedChapters int `json:"ordered_chapters"`
|
||||
// DeliveredChapters is null for a run whose order does not close whole chapters — a CHARACTER
|
||||
// order, which buys a prefix of one. Such a run has delivered no chapter, and `0` would read as
|
||||
// «nothing happened»; its progress is the `progress` pair, counted in the unit it was sold in.
|
||||
// OrderedChapters, OrderedUnits and DeliveredChapters are what the run bought and what it has
|
||||
// handed over. `ordered_chapters` is what `ceiling_chapters` was called until 0.11.0, renamed
|
||||
// because the word «ceiling» belonged to money and this number never was money — it is how much
|
||||
// BOOK was sold.
|
||||
//
|
||||
// ⛔ EXACTLY ONE OF THE FIRST TWO IS SET, and which one says what unit this run is measured in —
|
||||
// its bar, its delivery, all of it. A run sold in chapters carries `ordered_chapters`; one sold in
|
||||
// CHARACTERS carries `ordered_units` and a null here, because the chapter figure such a run used
|
||||
// to publish was the SPAN it reached into and overstated what was bought by up to a whole chapter.
|
||||
OrderedChapters *int `json:"ordered_chapters"`
|
||||
// OrderedUnits is how much a run sold in CHARACTERS bought, in the engine's own output units, and
|
||||
// null for a run sold in chapters. ⚠ It is not the number of characters the buyer typed: that
|
||||
// figure is resolved into units when the order is placed and is not stored.
|
||||
OrderedUnits *int `json:"ordered_units"`
|
||||
// DeliveredChapters is null for a run whose order was phrased in CHARACTERS. Such a run is
|
||||
// measured in units end to end, and `0` here would read as «nothing happened» while work is being
|
||||
// done and paid for; its progress is the `progress` pair, counted in the unit it was sold in.
|
||||
//
|
||||
// ⚠ NULL IS THE DISCRIMINATOR THE WIRE ACTUALLY HAS, and it is the only one: `ordered_chapters` is
|
||||
// a positive span for a character order too, so it separates nothing. Null here means «read the
|
||||
// bar as units»; `0` with `ordered_chapters: 0` means a re-pass.
|
||||
DeliveredChapters *int `json:"delivered_chapters"`
|
||||
// TermConsistencyFunded is the order form's promise, kept after the click: whether THIS run's
|
||||
// reservation has room for the book-wide pass that keeps a book's terms consistent. The form
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ func TestTheBookCardCarriesItsRunOrAnExplicitNull(t *testing.T) {
|
|||
finished := time.Unix(0, 0).UTC()
|
||||
lib.book.Revision = 1900
|
||||
lib.run = &pgstore.Run{ID: "run_1", BookID: "bk_1", Revision: 12, Status: "paused", VerifyBank: true,
|
||||
OrderedChapters: 100, Progress: pgstore.Progress{Done: 40, Total: 100},
|
||||
OrderedChapters: ptr(100), Progress: pgstore.Progress{Done: 40, Total: 100},
|
||||
PausedReason: "credit_exhausted", StartedAt: finished, FinishedAt: &finished}
|
||||
got = decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", ""))
|
||||
run, _ := got["run"].(map[string]any)
|
||||
|
|
@ -446,7 +446,7 @@ func TestAnExhaustedAccountAndAFinishedBookAreToldApartByTheCount(t *testing.T)
|
|||
// a missing field: what `ceiling_chapters` used to guard is now guarded by the hold, which the
|
||||
// server computes from the order it resolved itself.
|
||||
func TestARunTakesTheThreeKindsOfOrderAndDefaultsToTheWholeBook(t *testing.T) {
|
||||
rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", OrderedChapters: 100,
|
||||
rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", OrderedChapters: ptr(100),
|
||||
VerifyBank: true, StartedAt: time.Unix(0, 0).UTC()}}
|
||||
h := v0Server(t, &fakeLibrary{}, rn)
|
||||
|
||||
|
|
@ -690,7 +690,7 @@ func TestAVolumeBelowTheSchemaMinimumIsARejectedRequestAndNotAMovedBound(t *test
|
|||
// a server that refused the whole request would break a client generated against a later 0.x for a
|
||||
// field it was free to ignore.
|
||||
func TestAnUnknownRequestPropertyIsIgnoredRatherThanRefused(t *testing.T) {
|
||||
rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", OrderedChapters: 10}}
|
||||
rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", OrderedChapters: ptr(10)}}
|
||||
w := call(t, v0Server(t, &fakeLibrary{}, rn), "POST", "/v0/books/bk_1/runs",
|
||||
`{"stop_for_signing":true,"chapters":10,"a_field_from_a_later_minor":"x"}`)
|
||||
if w.Code != http.StatusAccepted {
|
||||
|
|
@ -935,8 +935,21 @@ func TestTheCharacterCountSaysWhichNumberItIs(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
// The cut's provenance travels beside it, verbatim, or as an explicit null when no manifest has
|
||||
// spoken. The client needs it to know what a chapter NUMBER is worth before it offers an order.
|
||||
}
|
||||
|
||||
// ⛔ THE CUT'S PROVENANCE ON THE BOOK CARD, and `null` there means «nobody has been asked», which is a
|
||||
// different fact from every word the vocabulary carries.
|
||||
//
|
||||
// It used to be asserted inside the character-count test, as a tenant with no name of its own — and a
|
||||
// contract that cites a pin by name cannot cite one named for something else. Moved rather than
|
||||
// copied: the assertions are the same, the subject is now findable.
|
||||
//
|
||||
// ⚠ THE BOOK HERE IS `not_started`, WHICH IS THE POINT. It has fully arrived and still answers `null`,
|
||||
// because no manifest has been read — so «null while the book is still arriving» is NARROWER than the
|
||||
// fact and a client that waits for arrival waits for the wrong thing. And `null` is not `none`: `none`
|
||||
// is a book the engine looked at and found one chapter in, which is a statement ABOUT the book;
|
||||
// `null` is the absence of any statement.
|
||||
func TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull(t *testing.T) {
|
||||
got := decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", ""))
|
||||
book, _ := got["book"].(map[string]any)
|
||||
if v, ok := book["structure"]; !ok || v != nil {
|
||||
|
|
@ -947,6 +960,13 @@ func TestTheCharacterCountSaysWhichNumberItIs(t *testing.T) {
|
|||
if book["structure"] != "declared" {
|
||||
t.Errorf("structure = %v, want the engine's own word", book["structure"])
|
||||
}
|
||||
// An unknown word travels VERBATIM too: the vocabulary grows additively, and a build that
|
||||
// re-wrote what it did not recognise would hide the growth from the client that must degrade on it.
|
||||
got = decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started", Structure: "delimited"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", ""))
|
||||
book, _ = got["book"].(map[string]any)
|
||||
if book["structure"] != "delimited" {
|
||||
t.Errorf("structure = %v, want the engine's word passed through untouched", book["structure"])
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ A RETIRED MEMBER IS REFUSED, NOT IGNORED, and the difference is a purchase.
|
||||
|
|
|
|||
|
|
@ -813,10 +813,24 @@ type Run struct {
|
|||
// ⚠ The column behind OrderedChapters is still called `ceiling_chapters`, and the word is the one
|
||||
// thing about it that was wrong: it never was a ceiling in the money sense — the money ceiling is
|
||||
// the hold — it was always how much book the run was sold.
|
||||
OrderedChapters int
|
||||
//
|
||||
// ⛔ NIL FOR A RUN SOLD IN CHARACTERS — see runOrderedChapters for why the row still carries a
|
||||
// number there and only the view is null. OrderedUnits is the figure such a run was sold in.
|
||||
OrderedChapters *int
|
||||
// OrderedUnits is how much this run bought in the engine's own output units, and nil for a run
|
||||
// bought in chapters. It is the other half of OrderedChapters: exactly one of the two is set, and
|
||||
// which one says what unit this run — its bar, its allowance, its delivery — is measured in.
|
||||
//
|
||||
// ⚠ It is NOT the number the buyer typed. A character order is resolved into units at admission
|
||||
// (`runs.resolveOrder` through `pricing.UnitsFor`) and the characters themselves are not stored
|
||||
// anywhere; a reloaded screen therefore answers in units even though the question was asked in
|
||||
// characters. Whether that echo is owed to the buyer is a product question, named in the pack's
|
||||
// report rather than decided here.
|
||||
OrderedUnits *int
|
||||
// DeliveredChapters is NIL for a run whose order does not close whole chapters: such a run has
|
||||
// delivered no CHAPTER, and reporting `0` would read as «nothing happened» rather than as «this
|
||||
// is not the unit this run is measured in». DeliveredUnits carries its progress instead.
|
||||
// is not the unit this run is measured in». Progress carries what it HAS done instead, and for
|
||||
// such a run its `done`/`total` are counted in units rather than chapters.
|
||||
DeliveredChapters *int
|
||||
// BondFunded is whether this run's hold has room for the book-wide consistency pass on top of
|
||||
// what it bought. It is the order form's promise, kept where it can still be read after the
|
||||
|
|
@ -841,13 +855,13 @@ type Run struct {
|
|||
// run and `b` its book. Written once because the three that used it wrote the same fourteen columns
|
||||
// three times, in three orders that had to stay in step by hand.
|
||||
const runRow = `r.id, b.id, b.revision, r.status, r.verify_bank, r.stop_requested_at is not null,
|
||||
r.ceiling_chapters, ` + runDelivered + `, r.bond_funded, ` + runDone + `, ` + runTotal + `, ` + runStage + `, r.eta_seconds,
|
||||
` + runOrderedChapters + `, r.ordered_units, ` + runDelivered + `, r.bond_funded, ` + runDone + `, ` + runTotal + `, ` + runStage + `, r.eta_seconds,
|
||||
coalesce(r.paused_reason, ''), coalesce(r.failure_reason, ''), r.started_at, r.finished_at`
|
||||
|
||||
// scanRun reads runRow into a Run, plus whatever the caller selected after it.
|
||||
func scanRun(row pgx.Row, out *Run, extra ...any) error {
|
||||
return row.Scan(append([]any{&out.ID, &out.BookID, &out.Revision, &out.Status, &out.VerifyBank,
|
||||
&out.StopRequested, &out.OrderedChapters, &out.DeliveredChapters, &out.BondFunded,
|
||||
&out.StopRequested, &out.OrderedChapters, &out.OrderedUnits, &out.DeliveredChapters, &out.BondFunded,
|
||||
&out.Progress.Done, &out.Progress.Total,
|
||||
&out.Progress.Stage, &out.Progress.ETASeconds, &out.PausedReason, &out.FailureReason,
|
||||
&out.StartedAt, &out.FinishedAt}, extra...)...)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,10 @@ func TestTheBookCardCarriesARevisionEitherWay(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if run == nil || run.ID != started.ID || run.OrderedChapters != 10 || run.VerifyBank {
|
||||
// ⚠ A CHAPTER-SHAPED run, so the figure is present; nil here would mean «this run is not measured
|
||||
// in chapters» (runOrderedChapters), which is a different answer from a wrong number.
|
||||
if run == nil || run.ID != started.ID || run.OrderedChapters == nil || *run.OrderedChapters != 10 ||
|
||||
run.VerifyBank {
|
||||
t.Fatalf("card run: %+v", run)
|
||||
}
|
||||
if run.PausedReason != "" || run.FinishedAt != nil {
|
||||
|
|
|
|||
|
|
@ -297,3 +297,61 @@ func TestEveryRunTheStoreHandsOutCarriesItsBooksRevision(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ A BOOK IS `not_started` BEFORE ITS PROVENANCE EXISTS, and the window is DESIGNED rather than a
|
||||
// race. `FinishParse` sets the status and ARMS THE READ-MODEL DEBT in one transaction — its own
|
||||
// comment says why: «this is the transaction that makes the book parsed, and a book that is parsed
|
||||
// owes a tree». The tree, and with it `structure`, arrive later, when the materializer pays that debt.
|
||||
//
|
||||
// ⚠ THIS PIN EXISTS BECAUSE A CONTRACT SENTENCE WAS WRITTEN OFF A DISPLAY TEST. The canon said
|
||||
// `Book.structure` is `null` for a book that is «fully uploaded, no manifest», citing a test whose
|
||||
// fake store returns a `not_started` book with no structure. That test is real and proves what it
|
||||
// proves — an empty column reaches the client as an explicit `null` — but it is a test of RENDERING:
|
||||
// it says nothing about whether a real book ever holds that pair. The illustration was a fixture read
|
||||
// as a product state. Here the pair is produced by the intake itself.
|
||||
//
|
||||
// ⚠ And it is not the only way to reach it: a manifest older than the `structure` field leaves the
|
||||
// column null for good (pgstore.Structure.Structure, «Empty when the engine did not say»), and
|
||||
// AddBook — the route for a file already on disk — creates a book `not_started` with no manifest at
|
||||
// all. Three roads, one pair; the status vocabulary's «cut, never run» is about the SOURCE having
|
||||
// been cut, not about the platform having read a manifest.
|
||||
//
|
||||
// Mutation caught: arming the read-model debt after the status instead of with it; a status written
|
||||
// only once the tree is materialised (the book would then be invisible to its owner while parsing
|
||||
// completed).
|
||||
func TestAParsedBookIsNotStartedBeforeItHasAnyProvenance(t *testing.T) {
|
||||
s, ctx := testDB(t)
|
||||
now := fundedAccount(t, s, ctx, "u1", "10")
|
||||
b := upload(t, s, ctx, "u1", now)
|
||||
if _, err := s.StartParsing(ctx, b.ID, 42, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claim, err := s.ClaimParse(ctx, b.ID, now, now.Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.FinishParse(ctx, b.ID, claim.At, ParsedBook{Chapters: 500, ChunkerVersion: "chunk-1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _, err := s.GetBook(ctx, "u1", b.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Status != "not_started" {
|
||||
t.Fatalf("a book whose parse just finished is %q", got.Status)
|
||||
}
|
||||
if got.Structure != "" {
|
||||
t.Fatalf("a book that has not been materialised carries provenance %q: the pair this pin is "+
|
||||
"about does not occur, and the contract's illustration of `null` is wrong in the other "+
|
||||
"direction", got.Structure)
|
||||
}
|
||||
// …and it is not that the book is somehow unfinished: it owes a tree, and the debt is what will
|
||||
// bring the provenance.
|
||||
owed, err := s.BooksOwedReadModel(ctx, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(owed) != 1 || owed[0].ID != b.ID {
|
||||
t.Fatalf("the parsed book does not owe a reading surface: %+v", owed)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import (
|
|||
func priced() Structure {
|
||||
in := twoChapters("k1")
|
||||
in.Structure = ingest.StructureDetected
|
||||
in.Price = &ingest.BookPrice{
|
||||
ExpectedUSD: 2_090_000, BookOnceUSD: 2_000_000, StepMaxUSD: 69_828, SourceChars: 3000,
|
||||
}
|
||||
in.Price = &Projection{Expected: 2_090_000, BookOnce: 2_000_000, StepMax: 69_828}
|
||||
// ⚠ ON THE CUT, NOT ON THE PRICE — the store keeps the two apart on purpose, and the type makes
|
||||
// that the only way to say it (see Projection).
|
||||
in.SourceChars = 3000
|
||||
// ⚠ The price lives on the UNIT and only there: a chapter's cost is the sum of the units still to
|
||||
// deliver, so a chapter-level copy would be a second carrier free to drift from it.
|
||||
in.Chapters[0].Units[0].Expected, in.Chapters[0].Units[0].SourceChars = 40_000, 1200
|
||||
|
|
@ -194,3 +195,88 @@ func TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy(t *testin
|
|||
t.Errorf("the cut's provenance did not land: %q", after.Structure)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ THE HONEST CHARACTER COUNT SURVIVES A PRICE THIS BUILD COULD NOT READ, and it did not until an
|
||||
// audit asked. `source_chars` was written only when the whole projection passed its witness — so a
|
||||
// manifest that named the text perfectly and failed on a MONEY key cost the screen its character
|
||||
// count too, and «Знаков» fell back to the intake's approximation, which for an EPUB counts the runes
|
||||
// of a ZIP archive. The two facts are independent: one is about the text, the other about money.
|
||||
//
|
||||
// ⚠ Since Projection was cut down to money alone, writing the count off the price is no longer
|
||||
// EXPRESSIBLE — the field it would be read from does not exist. What this pin still catches is the
|
||||
// other half of the same mistake: gating the write on the price's arrival.
|
||||
//
|
||||
// Mutation caught: writing a null `source_chars` whenever `in.Price` is nil.
|
||||
func TestTheCharacterCountSurvivesAPriceThisBuildCouldNotRead(t *testing.T) {
|
||||
s, ctx := testDB(t)
|
||||
book := readingBook(t, s, ctx, "u1")
|
||||
in := priced()
|
||||
in.Price = nil // the money did not arrive…
|
||||
in.SourceChars = 3000 // …and the engine's count of the text did (priced() sets it; said again here
|
||||
// because this test is ABOUT that figure and a reader should not have to go look)
|
||||
if err := s.SaveStructure(ctx, book, in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _, err := s.GetBook(ctx, "u1", book)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.SourceChars == nil || *got.SourceChars != 3000 {
|
||||
t.Fatalf("the engine's rune count was discarded with the price: %v", got.SourceChars)
|
||||
}
|
||||
// …and the book is still refused for SALE, which is the other half: the two facts are independent
|
||||
// in both directions.
|
||||
order, err := s.ReadBookForOrder(ctx, "u1", book)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if order.Priced {
|
||||
t.Error("a book whose price did not arrive reads as priced")
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ WORK DELIVERED BEYOND THE ORDER'S BOUNDARY MUST NOT CANCEL WORK STILL OWED INSIDE IT. The
|
||||
// allowance handed to the engine is «ordered minus delivered», and counting the delivered book-WIDE
|
||||
// while counting the ordered only up to the boundary makes the two describe different sets: a run
|
||||
// ordered through chapter 2 whose chapters 1 and 3 came back delivered fell to its floor of one unit
|
||||
// and half-delivered what was bought. It costs no money — settlement is by fact — and it breaks «you
|
||||
// bought N», which is the promise this pack exists to make true.
|
||||
//
|
||||
// Mutation caught: `deliveredWithinOrder` counting the book instead of the order.
|
||||
func TestDeliveryBeyondTheOrderDoesNotCancelWorkOwedInsideIt(t *testing.T) {
|
||||
s, ctx := testDB(t)
|
||||
book := readingBook(t, s, ctx, "u1")
|
||||
if err := s.SaveStructure(ctx, book, priced()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The order runs through chapter ONE (two units). Chapter TWO — beyond it — comes back delivered.
|
||||
var c1 string
|
||||
if err := s.pool.QueryRow(ctx,
|
||||
`select id from chapters where book_id = $1 and number = 1`, book).Scan(&c1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `
|
||||
update books set ordered_at = now(), ordered_through_chapter_id = $2,
|
||||
ordered_through_chapter_number = 1 where id = $1`, book, c1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `
|
||||
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
|
||||
values ($1, 2, 0, 'edit', true, false, now())`, book); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ordered, delivered int
|
||||
var resolved bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
select `+orderedUnits+`, `+orderResolved+`, `+deliveredWithinOrder+`
|
||||
from books b where b.id = $1`, book).Scan(&ordered, &resolved, &delivered); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resolved || ordered != 2 {
|
||||
t.Fatalf("the order through chapter one resolves to %d units (resolved %v), want 2", ordered, resolved)
|
||||
}
|
||||
if delivered != 0 {
|
||||
t.Errorf("a chapter delivered BEYOND the order counted as %d units of it: the allowance would "+
|
||||
"fall to its floor and half-deliver what was bought", delivered)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,11 +76,44 @@ type Structure struct {
|
|||
// whole one. A book with no projection is one the platform REFUSES to sell rather than one it
|
||||
// prices with a constant (pricing's own package comment), so nil is written through: a projection
|
||||
// that goes stale is worse than one that is honestly absent.
|
||||
Price *ingest.BookPrice
|
||||
Price *Projection
|
||||
// Structure is where the chapter boundaries came from, verbatim from the engine. Empty when the
|
||||
// engine did not say — a manifest older than the landing — and stored as such: the decision it
|
||||
// feeds (ingest.ChapterOrdersOffered) treats what it does not know as untrusted.
|
||||
Structure string
|
||||
// SourceChars is the engine's count of the book's ingested text in runes.
|
||||
//
|
||||
// ⛔ CARRIED APART FROM Price, AND THAT IS THE POINT. It is a property of the CUT, not of the
|
||||
// money: a manifest can name the text honestly and still fail the price witness (a renamed money
|
||||
// key, a chapter that lost its bill). Read off `Price`, such a book lost its honest character
|
||||
// count too and fell back to the intake's approximation — a number that for an EPUB counts the
|
||||
// runes of a ZIP archive — for a reason that has nothing to do with counting. Zero means the
|
||||
// engine did not say.
|
||||
SourceChars int64
|
||||
}
|
||||
|
||||
// Projection is the MONEY half of the engine's a-priori figure, as the store holds it.
|
||||
//
|
||||
// ⛔ IT CARRIES NO CHARACTER COUNT, and the absence is the whole reason this type exists rather than
|
||||
// the wire's own `ingest.BookPrice`. The engine publishes `source_chars` INSIDE its price object,
|
||||
// because for the engine both are outputs of one projection; the store keeps them apart
|
||||
// (Structure.SourceChars) because for the store they answer to different witnesses — a manifest can
|
||||
// name the text honestly and fail on a MONEY key, and a book that lost its bill has no business
|
||||
// losing its character count too.
|
||||
//
|
||||
// ⚠ Reusing the wire struct here put the same figure in TWO fields of one value, and a caller then
|
||||
// filled one and not the other — which is not a hypothetical: it is how
|
||||
// TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy went red. One fact, one carrier.
|
||||
type Projection struct {
|
||||
// Expected is the whole book's expected bill, book-level passes INCLUDED.
|
||||
Expected money.MicroUSD
|
||||
// BookOnce is the part of Expected that belongs to the BOOK and not to any chapter — a flat bond
|
||||
// on the shipped arm, which is why an ORDER is priced from the chapters' own bills instead (see
|
||||
// pricing.Hold and ingest.BookPrice.BookOnceUSD for the measurement behind it).
|
||||
BookOnce money.MicroUSD
|
||||
// StepMax is the largest single reservation any one call of this book can ask for: the floor of
|
||||
// every hold, because a ceiling below it admits nothing.
|
||||
StepMax money.MicroUSD
|
||||
}
|
||||
|
||||
// StructureChapter is one chapter and its pairs, in reading order.
|
||||
|
|
@ -161,11 +194,17 @@ func (s *Store) SaveStructure(ctx context.Context, bookID string, in Structure)
|
|||
// Nulls are written through when the book is not priced. A stale projection kept beside a fresh
|
||||
// tree would sell yesterday's book; an absent one refuses the sale, which is the honest half.
|
||||
var expected, bookOnce, stepMax *int64
|
||||
var sourceChars *int64
|
||||
if in.Price != nil {
|
||||
e, b, sm := int64(in.Price.ExpectedUSD), int64(in.Price.BookOnceUSD), int64(in.Price.StepMaxUSD)
|
||||
sc := in.Price.SourceChars
|
||||
expected, bookOnce, stepMax, sourceChars = &e, &b, &sm, &sc
|
||||
e, b, sm := int64(in.Price.Expected), int64(in.Price.BookOnce), int64(in.Price.StepMax)
|
||||
expected, bookOnce, stepMax = &e, &b, &sm
|
||||
}
|
||||
// ⚠ INDEPENDENT OF THE PRICE — see Structure.SourceChars. A book the engine could not price is
|
||||
// still a book whose text the engine counted, and the screen's «Знаков» has no business
|
||||
// falling back to the intake's approximation because a MONEY key was renamed.
|
||||
var sourceChars *int64
|
||||
if in.SourceChars > 0 {
|
||||
sc := in.SourceChars
|
||||
sourceChars = &sc
|
||||
}
|
||||
var structureWord *string
|
||||
if in.Structure != "" {
|
||||
|
|
@ -632,9 +671,12 @@ const (
|
|||
// top for the same reason the bar is: free and carried units ride outside the grant (volume.go),
|
||||
// so a run can finish more than it was sold, and a figure above what was bought would read as an
|
||||
// overdelivery rather than as the rounding it is. Zero for a re-pass, which buys no chapters.
|
||||
// ⚠ AND WHAT SUCH A RUN HAS DELIVERED IS COUNTED IN UNITS TOO — reported to the client as
|
||||
// `delivered_units` beside a null `delivered_chapters`, because a run that bought a prefix of a
|
||||
// chapter has delivered no CHAPTER and saying «0» would read as «nothing happened».
|
||||
// ⚠ NULL — NOT ZERO — FOR A UNIT-SHAPED ORDER, and there is no second figure beside it. A run
|
||||
// that bought a prefix of a chapter has delivered no CHAPTER, and «0» would read as «nothing
|
||||
// happened» when work was done and paid for. What such a run has delivered is carried by the
|
||||
// BAR instead, whose `done`/`total` are counted in units for exactly this run (see runDone) —
|
||||
// so the two figures a client gets are a null chapter count and a unit-shaped fraction, and
|
||||
// nothing on the wire is called `delivered_units`.
|
||||
runDelivered = `(case when r.ordered_units is not null then null
|
||||
when r.ceiling_chapters = 0 then 0
|
||||
else least(greatest((case when ` + editWave + ` then ` + editChapters + ` - r.chapters_before
|
||||
|
|
@ -646,6 +688,25 @@ const (
|
|||
then 'editing' else 'drafting' end)`
|
||||
)
|
||||
|
||||
// runOrderedChapters is what the contract calls `ordered_chapters`: how much BOOK this run was sold,
|
||||
// in chapters — and NULL for a run that was not sold in chapters at all.
|
||||
//
|
||||
// ⛔ THE NULL IS THE POINT, and the figure it replaces was a lie of a particular kind: not a wrong
|
||||
// number, but a right number answering a question nobody asked. The column underneath
|
||||
// (`ceiling_chapters`) holds, for an order phrased in CHARACTERS, the chapter SPAN — how many
|
||||
// chapters the order reaches INTO — and that overstates what was bought by as much as a whole
|
||||
// chapter: an order of one unit out of four in the first chapter stored `1`, which is the cheapest
|
||||
// order there is and the one a person tries the service with. Measured, not reasoned: see
|
||||
// `runs.TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`.
|
||||
//
|
||||
// ⚠ IT NULLS THE VIEW AND NOT THE COLUMN, deliberately. `ceiling_chapters = 0` is the RE-PASS's own
|
||||
// mark, and three places read it as exactly that — `runs.maxUnitsFor`, the reconciler's resume gate,
|
||||
// and this package's own admission check. Writing a zero or a null into the row would make a
|
||||
// character order indistinguishable from a re-pass to all three, and in the first of them the cost is
|
||||
// money: zero is the engine's word for «no volume bound at all». So the row keeps the span, every
|
||||
// internal reader keeps its meaning, and only what goes OUT changes.
|
||||
const runOrderedChapters = `(case when r.ordered_units is not null then null else r.ceiling_chapters end)`
|
||||
|
||||
// newestRun is the order in which a book RESOLVES to one of its runs: the LIVE one first, however the
|
||||
// clocks fell, and only then the newest-started.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1403,6 +1403,18 @@ func TestARePassRunsBarIsOneUnitOfWork(t *testing.T) {
|
|||
t.Fatalf("a fresh re-pass opens at %d/%d %q, want 0/1 re_pass",
|
||||
run.Progress.Done, run.Progress.Total, run.Progress.Stage)
|
||||
}
|
||||
// ⛔ THE TWO VOLUME FIGURES A RE-PASS PUTS ON THE WIRE, pinned because the CONTRACT asserts them
|
||||
// and a contract may only assert what a pin holds. Both are ZERO and neither is null: a re-pass
|
||||
// was ordered in chapters (it buys none) and it is not measured in units, so `ordered_units` is
|
||||
// the one that is absent. This is what makes `ordered_chapters == 0` the re-pass's own mark — the
|
||||
// FIRST check a client owes, before it reads `delivered_chapters`, which here is a number and
|
||||
// would otherwise be taken for a chapter count.
|
||||
if run.OrderedChapters == nil || *run.OrderedChapters != 0 {
|
||||
t.Errorf("the start receipt of a re-pass reports ordered_chapters %v, want 0", run.OrderedChapters)
|
||||
}
|
||||
if run.OrderedUnits != nil {
|
||||
t.Errorf("a re-pass reports ordered_units %d; it is not measured in units", *run.OrderedUnits)
|
||||
}
|
||||
exec(t, s, ctx, `update runs set status = 'ready', finished_at = $2 where id = $1`,
|
||||
run.ID, at.Add(time.Minute))
|
||||
got, err := s.ReadRun(ctx, "u1", run.ID)
|
||||
|
|
@ -1413,6 +1425,22 @@ func TestARePassRunsBarIsOneUnitOfWork(t *testing.T) {
|
|||
t.Fatalf("a finished re-pass reads %d/%d %q, want 1/1 re_pass",
|
||||
got.Progress.Done, got.Progress.Total, got.Progress.Stage)
|
||||
}
|
||||
// ⚠ AND THE READ PATH SAYS THE SAME AS THE RECEIPT — they are two different SQL expressions
|
||||
// (`insertRun`'s RETURNING and `runOrderedChapters` over the lateral), and a contract that
|
||||
// promises one shape must be held to it on both.
|
||||
if got.OrderedChapters == nil || *got.OrderedChapters != 0 {
|
||||
t.Errorf("a re-pass reads back ordered_chapters %v, want 0", got.OrderedChapters)
|
||||
}
|
||||
if got.OrderedUnits != nil {
|
||||
t.Errorf("a re-pass reads back ordered_units %d", *got.OrderedUnits)
|
||||
}
|
||||
// ⛔ ZERO, NOT NULL — and this is the value that makes `delivered_chapters` USELESS as the sole
|
||||
// discriminator of the bar's unit: a re-pass answers with a NUMBER here, so the rule «a number
|
||||
// means the counters are chapters» would have a client render a re-pass as «0 of 1 chapters»,
|
||||
// which the contract's own Progress paragraph forbids.
|
||||
if got.DeliveredChapters == nil || *got.DeliveredChapters != 0 {
|
||||
t.Errorf("a re-pass reads back delivered_chapters %v, want 0 rather than null", got.DeliveredChapters)
|
||||
}
|
||||
if _, err := s.StartRun(ctx, StartRunInput{UserID: "u1", BookID: book, OrderedChapters: 0,
|
||||
Ceiling: money.MicroUSD(100_000), Now: at.Add(time.Hour)}, 0, nil); err == nil {
|
||||
t.Fatal("a zero-chapter run without the re-pass consent was admitted")
|
||||
|
|
|
|||
|
|
@ -45,10 +45,15 @@ type StartRunInput struct {
|
|||
// a blanket yes (D20.2-Q2).
|
||||
Resnapshot bool
|
||||
AcceptRebill money.MicroUSD
|
||||
// OrderedUnits is set ONLY for a run whose order does not close whole chapters — a CHARACTER
|
||||
// order. Nil is the ordinary shape and leaves the run's bar counted in chapters, exactly as
|
||||
// before this pack. See migration 00033 for why such a run needs a bar of its own: counted in
|
||||
// chapters it reads `0/N` for its whole life, which is the state the admission refuses a book for.
|
||||
// OrderedUnits is set for every run whose order was phrased in CHARACTERS, and nil for every other
|
||||
// shape. Nil is the ordinary case and leaves the run's bar counted in chapters, exactly as before
|
||||
// this pack. See migration 00033 for why such a run needs a bar of its own: counted in chapters an
|
||||
// order that stops inside a chapter reads `0/N` for its whole life, which is the state the
|
||||
// admission refuses a book for.
|
||||
//
|
||||
// ⚠ THE CONDITION IS THE PHRASING, NOT WHERE THE ORDER LANDED. An earlier edition of this line
|
||||
// said «only for a run whose order does not close whole chapters», and that is measurably false —
|
||||
// see pricing.Quote.UnitShaped for the measurement and for why the phrasing is the right test.
|
||||
OrderedUnits *int
|
||||
// BondFunded is whether this run's hold has room for the book-level consistency passes on top of
|
||||
// the work it bought. Recorded on the RUN because the order form's answer is a QUOTE and the
|
||||
|
|
@ -153,12 +158,13 @@ func (s *Store) StartRun(ctx context.Context, in StartRunInput, journalOffset in
|
|||
where c.book_id = b.id and c.units_total > 0 and c.units_draft_done >= c.units_total),
|
||||
` + bookUnitsEditDone + `, ` + bookUnitsDraftDone + `
|
||||
from books b where b.id = $2 and b.owner_id = $6
|
||||
returning id, book_id, revision, status, verify_bank, ceiling_chapters,
|
||||
coalesce(paused_reason, ''), started_at, finished_at`
|
||||
returning id, book_id, revision, status, verify_bank,
|
||||
(case when ordered_units is not null then null else ceiling_chapters end),
|
||||
ordered_units, coalesce(paused_reason, ''), started_at, finished_at`
|
||||
err := tx.QueryRow(ctx, insertRun, runID, in.BookID, in.VerifyBank, in.OrderedChapters, in.Now, in.UserID,
|
||||
in.Resnapshot, int64(in.AcceptRebill), in.BondFunded, in.OrderedUnits).
|
||||
Scan(&out.ID, &out.BookID, &out.Revision, &out.Status, &out.VerifyBank, &out.OrderedChapters,
|
||||
&out.PausedReason, &out.StartedAt, &out.FinishedAt)
|
||||
&out.OrderedUnits, &out.PausedReason, &out.StartedAt, &out.FinishedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrNoBook // the book is missing, or it is not this account's
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,9 +140,20 @@ type Quote struct {
|
|||
ThroughChapter int
|
||||
// Units is how many output units the order buys — the figure `--max-units` is derived from.
|
||||
Units int
|
||||
// UnitShaped says the order does not close whole chapters: it stops inside one. Only a CHARACTER
|
||||
// order can, and it is the reason such a run needs a bar counted in units — in chapters it would
|
||||
// read `0/N` for its whole life, because a chapter counts only once every unit in it is done.
|
||||
// UnitShaped says the order was phrased in CHARACTERS, and therefore is measured in units all the
|
||||
// way through: the bar counts units, and `delivered_chapters` is null.
|
||||
//
|
||||
// ⚠ IT IS ABOUT THE PHRASING, NOT ABOUT WHERE THE ORDER LANDED — an earlier edition of this line
|
||||
// said «does not close whole chapters», and that is measurably false: `UnitsFor` returns the first
|
||||
// unit whose running sum covers what was asked, so an order lands on a chapter boundary roughly as
|
||||
// often as chapters divide units (a third of them at three units a chapter), and such an order
|
||||
// closes whole chapters while staying unit-shaped. Pinned by
|
||||
// `runs.TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`.
|
||||
//
|
||||
// The reason to measure in the unit the buyer SPOKE, rather than in whatever the order happens to
|
||||
// cover, is that two purchases a person cannot tell apart must not render differently. The reason
|
||||
// it cannot be chapters is older: a chapter counts only once every unit in it is done, so an order
|
||||
// that stops inside one would read `0/N` for its whole life.
|
||||
UnitShaped bool
|
||||
// Expected is what the engine expects the ordered work to be BILLED. It is the honest half of the
|
||||
// pair a buyer is shown («ожидаемо ≈ …, зарезервируем до …»).
|
||||
|
|
|
|||
|
|
@ -316,6 +316,9 @@ func (s *Service) refreshStructure(ctx context.Context, bookID, workdir string,
|
|||
// the old rows are deleted rather than left alone and the text is gone for good.
|
||||
in := pgstore.Structure{ManifestKey: manifest.Key, TextRead: textErr == nil,
|
||||
Structure: manifest.Structure,
|
||||
// The engine's own rune count, carried whether or not the book could be PRICED: it is a fact
|
||||
// about the text, and the price witness is about money (pgstore.Structure.SourceChars).
|
||||
SourceChars: manifestSourceChars(manifest),
|
||||
Chapters: make([]pgstore.StructureChapter, 0, len(manifest.Chapters))}
|
||||
// ⚠ ALL OR NOTHING, and the decision is Priced's rather than this loop's. A projection is written
|
||||
// only when the whole of it was read — the book figure, a price on every chapter, and the two
|
||||
|
|
@ -324,7 +327,11 @@ func (s *Service) refreshStructure(ctx context.Context, bookID, workdir string,
|
|||
// moved nothing. So `priced` gates the per-chapter numbers too, and a book the engine could not
|
||||
// price is stored as not priced rather than as free.
|
||||
if p, ok := manifest.Priced(); ok {
|
||||
in.Price = &p
|
||||
// ⚠ MAPPED FIELD BY FIELD RATHER THAN CARRIED WHOLE, and `source_chars` is deliberately not
|
||||
// among them: the store holds it on the CUT (in.SourceChars, set above), so that a book whose
|
||||
// money did not arrive keeps the honest count of its text. pgstore.Projection carries no such
|
||||
// field, which is what makes the omission impossible to make by accident.
|
||||
in.Price = &pgstore.Projection{Expected: p.ExpectedUSD, BookOnce: p.BookOnceUSD, StepMax: p.StepMaxUSD}
|
||||
} else if why := manifest.PriceRefusal(); why != "" {
|
||||
// ⛔ SAID OUT LOUD, because the consequence is a book that materializes perfectly and can never
|
||||
// be SOLD. Everything else about it lands — the tree, the text, the reading surface — and only
|
||||
|
|
@ -380,3 +387,15 @@ func (s *Service) refreshBank(ctx context.Context, bookID, bankExport string) er
|
|||
}
|
||||
return s.Store.SaveBank(ctx, bookID, bank.Terms)
|
||||
}
|
||||
|
||||
// manifestSourceChars is the book's ingested text in runes as the engine counted it, or 0.
|
||||
//
|
||||
// It reads the figure off the price block because that is where the engine publishes it — but it
|
||||
// deliberately does NOT ask whether the projection as a whole was readable. A renamed money key must
|
||||
// not cost the screen its honest character count.
|
||||
func manifestSourceChars(m ingest.Manifest) int64 {
|
||||
if m.Price == nil {
|
||||
return 0
|
||||
}
|
||||
return m.Price.SourceChars
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"textmachine/platform/internal/ingest"
|
||||
"textmachine/platform/internal/money"
|
||||
"textmachine/platform/internal/pgstore"
|
||||
)
|
||||
|
||||
|
|
@ -644,3 +645,77 @@ func TestTheMaterializationBackoffGrowsAndIsCapped(t *testing.T) {
|
|||
t.Errorf("the retry of a long-dead book is %v, want the cap", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ THE SEAM BETWEEN THE ENGINE'S DOCUMENT AND THE STORE'S ROW, and it went untested until it broke.
|
||||
//
|
||||
// Both halves had tests: `ingest` decides whether a manifest is priced, `pgstore` writes what it is
|
||||
// handed. What nobody asked was whether this function hands over the RIGHT THING — and the answer was
|
||||
// no. `pgstore.Structure` carried the book's character count in two places at once (its own field and
|
||||
// the wire struct's), this mapping filled one of them, and the row went in null. The type no longer
|
||||
// admits that, so what is pinned here is the mapping's SHAPE: every money figure arrives, and the
|
||||
// character count arrives on the CUT, where it survives a price this build cannot read.
|
||||
//
|
||||
// Mutation caught: dropping a member of the Projection literal; reading SourceChars off the price
|
||||
// object again; gating the character count on `Priced`.
|
||||
func TestTheProjectionReachesTheStoreWholeAndTheCharacterCountTravelsBesideIt(t *testing.T) {
|
||||
m := tree()
|
||||
m.Structure = ingest.StructureDetected
|
||||
m.Price = &ingest.BookPrice{ExpectedUSD: 2_090_000, BookOnceUSD: 2_000_000, StepMaxUSD: 69_828, SourceChars: 3000}
|
||||
pricedTree(&m)
|
||||
store := &fakeStore{}
|
||||
svc := &Service{Store: store, Binary: "tmctl", Engine: &fakeEngine{manifest: m, noEnvelope: true}}
|
||||
_ = svc.Refresh(t.Context(), owedBook(t.TempDir())) // the bank half fails; the tree lands anyway
|
||||
if !store.saved {
|
||||
t.Fatal("the tree did not land at all")
|
||||
}
|
||||
got := store.structure
|
||||
if got.Price == nil {
|
||||
t.Fatal("a manifest this build read whole reached the store with no projection")
|
||||
}
|
||||
if got.Price.Expected != 2_090_000 || got.Price.BookOnce != 2_000_000 || got.Price.StepMax != 69_828 {
|
||||
t.Errorf("a money figure was lost in the mapping: %+v", *got.Price)
|
||||
}
|
||||
// ⛔ ON THE CUT, and this is the assertion the defect would have failed: the count is a property of
|
||||
// the TEXT and the store keeps it apart from the money.
|
||||
if got.SourceChars != 3000 {
|
||||
t.Errorf("the engine's rune count reached the store as %d", got.SourceChars)
|
||||
}
|
||||
if got.Structure != ingest.StructureDetected {
|
||||
t.Errorf("the cut's provenance reached the store as %q", got.Structure)
|
||||
}
|
||||
}
|
||||
|
||||
// …and the other direction of the same independence: a manifest whose MONEY this build cannot read
|
||||
// still delivers the count of its text. The refusal is `ingest`'s (a chapter with no price), so what
|
||||
// is pinned here is that this mapping does not widen it into the character count.
|
||||
func TestAPriceThisBuildCannotReadDoesNotCostTheStoreItsCharacterCount(t *testing.T) {
|
||||
m := tree()
|
||||
m.Structure = ingest.StructureDetected
|
||||
m.Price = &ingest.BookPrice{ExpectedUSD: 2_090_000, BookOnceUSD: 2_000_000, StepMaxUSD: 69_828, SourceChars: 3000}
|
||||
pricedTree(&m)
|
||||
// One unit loses its bill, and `ingest` then refuses the projection as a whole — the half-read
|
||||
// shape is the dangerous one. The book's own `source_chars` is untouched and must still travel.
|
||||
m.Chapters[0].Units[0].Price = nil
|
||||
store := &fakeStore{}
|
||||
svc := &Service{Store: store, Binary: "tmctl", Engine: &fakeEngine{manifest: m, noEnvelope: true}}
|
||||
_ = svc.Refresh(t.Context(), owedBook(t.TempDir()))
|
||||
if store.structure.Price != nil {
|
||||
t.Errorf("a projection this build could not read whole was written anyway: %+v", *store.structure.Price)
|
||||
}
|
||||
if store.structure.SourceChars != 3000 {
|
||||
t.Errorf("the character count went down with the price: %d", store.structure.SourceChars)
|
||||
}
|
||||
}
|
||||
|
||||
// pricedTree gives `tree()` a projection its own witness accepts: 30_000 micro-USD and 1000 runes a
|
||||
// unit, the chapters' roll-up summing to the book's figure less the flat book-level bond.
|
||||
func pricedTree(m *ingest.Manifest) {
|
||||
for i := range m.Chapters {
|
||||
total := money.MicroUSD(0)
|
||||
for j := range m.Chapters[i].Units {
|
||||
m.Chapters[i].Units[j].Price = &ingest.UnitPrice{ExpectedUSD: 30_000, SourceChars: 1000}
|
||||
total += 30_000
|
||||
}
|
||||
m.Chapters[i].Price = &ingest.UnitPrice{ExpectedUSD: total, SourceChars: int64(len(m.Chapters[i].Units)) * 1000}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func TestAWholeBookOrderCarriesNoVolumeBound(t *testing.T) {
|
|||
// respawned after a restart would be handed its whole order a second time and the promise «you
|
||||
// bought N units» would stop being about N.
|
||||
//
|
||||
// Mutation caught: maxUnitsFor ignoring DeliveredUnits.
|
||||
// Mutation caught: maxUnitsFor ignoring SpawnOrder.Delivered.
|
||||
func TestTheVolumeAllowanceIsWhatIsLeftOfTheOrderAtEverySpawn(t *testing.T) {
|
||||
f := newFixture(t, "10", 500)
|
||||
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(10)})
|
||||
|
|
@ -627,8 +627,8 @@ func TestTheVolumeAllowanceCountsUnitsAndNotChapters(t *testing.T) {
|
|||
}
|
||||
// …and what was SOLD is still counted in chapters, because that is what the buyer chose and what
|
||||
// the run's own bar is measured in.
|
||||
if got := f.run(t, run.ID).OrderedChapters; got != 2 {
|
||||
t.Errorf("the run says it bought %d chapters", got)
|
||||
if got := f.run(t, run.ID).OrderedChapters; got == nil || *got != 2 {
|
||||
t.Errorf("the run says it bought %v chapters", got)
|
||||
}
|
||||
// The hold is the units' money, not the chapters': eight units at the fixture's rate.
|
||||
if got, want := f.account(t).Reserved, fixtureHold(8); got != want {
|
||||
|
|
@ -712,6 +712,16 @@ func TestAChapterOrderKeepsTheBarItAlwaysHad(t *testing.T) {
|
|||
if got.DeliveredChapters == nil || *got.DeliveredChapters != 0 {
|
||||
t.Errorf("a chapter order reports delivered chapters as %v, want 0 rather than null", got.DeliveredChapters)
|
||||
}
|
||||
// …and the pair that says which unit this run is measured in points the other way from the
|
||||
// character order's: the chapter figure is present, the unit figure is not. Exactly one is set,
|
||||
// and this is the half that would go silently null if the branch asked «is this a partial order»
|
||||
// instead of «was this sold in characters».
|
||||
if got.OrderedChapters == nil || *got.OrderedChapters != 2 {
|
||||
t.Errorf("a two-chapter order publishes ordered_chapters %v, want 2", got.OrderedChapters)
|
||||
}
|
||||
if got.OrderedUnits != nil {
|
||||
t.Errorf("a run sold in chapters publishes ordered_units %d; the two are exclusive", *got.OrderedUnits)
|
||||
}
|
||||
// One whole chapter delivered moves both the bar and the delivered count.
|
||||
if _, err := f.store.Pool().Exec(f.ctx, `
|
||||
update chapters set units_draft_done = 2, units_edit_done = 2
|
||||
|
|
@ -726,3 +736,94 @@ func TestAChapterOrderKeepsTheBarItAlwaysHad(t *testing.T) {
|
|||
t.Errorf("one whole chapter through both passes reads %d/%d", got.Progress.Done, got.Progress.Total)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ A CHARACTER ORDER PUBLISHES ITS VOLUME IN THE UNIT IT WAS SOLD IN, and says NOTHING in chapters.
|
||||
//
|
||||
// ⚠ THIS TEST REPLACES ONE THAT PINNED THE OPPOSITE, and the replacement is ordered rather than
|
||||
// convenient (D39.183 — declared in the pack's report). Its predecessor,
|
||||
// `TestACharacterOrderReportsTheChapterSpanItReachesIntoAndNotZero`, pinned the SPAN — how many
|
||||
// chapters the order reaches into — because that was what the build published and the contract
|
||||
// described a zero. Measuring it is what killed it: the span OVERSTATES what was bought by as much as
|
||||
// a whole chapter, and worst on the cheapest order there is. One unit out of four in the first
|
||||
// chapter published «1 chapter», which a person reads as «my money reached the end of chapter one».
|
||||
// The guarantee did not disappear — it moved: what the buyer got is now published as `ordered_units`,
|
||||
// in the unit they asked in, and `ordered_chapters` is null, which is the honest answer to a question
|
||||
// that does not apply.
|
||||
//
|
||||
// ⚠ The row underneath still holds the span — see pgstore.runOrderedChapters — because
|
||||
// `ceiling_chapters = 0` is the RE-PASS's mark and three internal readers depend on it.
|
||||
//
|
||||
// Mutation caught: publishing the span again; nulling BOTH figures; nulling the chapter figure for a
|
||||
// CHAPTER order (the branch is `ordered_units is not null`, not «is this a partial order»).
|
||||
func TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn(t *testing.T) {
|
||||
f := newFixture(t, "10", 500)
|
||||
book := multiUnitBook(t, f, "span", 3, 4) // three chapters, four units each, 1000 runes a unit
|
||||
// 4500 runes ⇒ five units ⇒ the whole of chapter one and one unit of chapter two.
|
||||
chars := int64(4500)
|
||||
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Characters: &chars})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := f.run(t, run.ID)
|
||||
if got.OrderedChapters != nil {
|
||||
t.Errorf("a run sold in characters reports ordered_chapters %d: the chapter figure it used to "+
|
||||
"publish was the SPAN it reached into, and it overstated what was bought", *got.OrderedChapters)
|
||||
}
|
||||
if got.OrderedUnits == nil || *got.OrderedUnits != 5 {
|
||||
t.Errorf("a five-unit order publishes ordered_units %v, want 5 — the unit it was sold in", got.OrderedUnits)
|
||||
}
|
||||
// …and the two figures that DO speak the order's own unit say so, which is what makes the span
|
||||
// readable rather than misleading: the bar counts units and no chapter is claimed as delivered.
|
||||
if got.Progress.Total != 10 {
|
||||
t.Errorf("the bar of a five-unit order over two waves reads /%d, want /10", got.Progress.Total)
|
||||
}
|
||||
if got.DeliveredChapters != nil {
|
||||
t.Errorf("delivered_chapters is %d rather than null on a unit-shaped run", *got.DeliveredChapters)
|
||||
}
|
||||
// ⚠ AND THE ORDER CLOSED A WHOLE CHAPTER — chapter one, all four of its units — while still being
|
||||
// unit-shaped. So «unit-shaped» means «phrased in characters», NOT «closes no whole chapter»,
|
||||
// which is how three doc comments and the canon describe it.
|
||||
if got.Progress.Done != 0 {
|
||||
t.Errorf("nothing has been delivered yet and the bar reads %d", got.Progress.Done)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ A BOOK WITH NOTHING LEFT IS REFUSED, AND THAT REFUSAL IS WHAT MAKES `ordered_chapters: 0` MEAN
|
||||
// «RE-PASS» AND NOTHING ELSE.
|
||||
//
|
||||
// The contract now tells a client to read the bar's unit off that field, and its FIRST case is «zero
|
||||
// ⇒ a re-pass, `total` is 1, do not render it as chapters». That rule is only safe because no other
|
||||
// shape can put a zero there — and one nearly can: `pricing.Quote` clamps a whole-book order to what
|
||||
// remains, so over a fully delivered book it quotes ZERO chapters, and the store's own admission
|
||||
// check refuses a zero only when `Resnapshot` is false, which a continuation over a moved bank sets
|
||||
// to true. The guard in `Start` is what closes that door, ahead of the store.
|
||||
//
|
||||
// Without it such a run would land with `ceiling_chapters = 0` and be INDISTINGUISHABLE FROM A
|
||||
// RE-PASS on the wire: bar 0/1, `stage: re_pass`, `delivered_chapters: 0`. The buyer would be shown
|
||||
// somebody else's shape of work.
|
||||
//
|
||||
// Mutation caught: dropping the `quote.Chapters == 0` guard; moving it after the hold is taken;
|
||||
// letting Quote return the remainder count without clamping.
|
||||
func TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun(t *testing.T) {
|
||||
f := newFixture(t, "10", 500)
|
||||
book := multiUnitBook(t, f, "done", 3, 2)
|
||||
// Every unit of every chapter delivered in the wave the book is measured in.
|
||||
if _, err := f.store.Pool().Exec(f.ctx, `
|
||||
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
|
||||
select $1, c, u - 1, 'edit', true, false, now()
|
||||
from generate_series(1, 3) c, generate_series(1, 2) u`, book); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book})
|
||||
if err == nil {
|
||||
t.Fatal("a fully delivered book was sold a run; on the wire it would be indistinguishable " +
|
||||
"from a re-pass, because both carry ordered_chapters 0")
|
||||
}
|
||||
if !errors.Is(err, ErrCeilingOutOfBounds) {
|
||||
t.Errorf("the refusal came as %v; the client needs the one that means «re-read the options»", err)
|
||||
}
|
||||
// …and the money did not move: the refusal is ahead of the hold.
|
||||
if got := f.account(t).Reserved; got != 0 {
|
||||
t.Errorf("a refused order reserved %s", got.USD())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -474,8 +474,14 @@ func (s *Service) Start(ctx context.Context, in StartRequest) (pgstore.Run, erro
|
|||
UserID: in.UserID,
|
||||
BookID: in.BookID,
|
||||
VerifyBank: in.VerifyBank,
|
||||
// The RUN's own bar is still counted in chapters (canon §Progress), so it carries how many the
|
||||
// order spans; the ORDER itself lives on the book, in units.
|
||||
// How many chapters the order SPANS. For an order phrased in chapters that is what was bought,
|
||||
// and it is what the bar counts and what the wire publishes. For one phrased in CHARACTERS it
|
||||
// is only stored: the span overstates what was bought — a single unit of a four-unit chapter
|
||||
// spans `1` — so the wire publishes `ordered_units` instead and nulls this
|
||||
// (pgstore.runOrderedChapters). ⚠ It is never 0 for either shape (`max(chapters, 1)` in
|
||||
// QuoteUnits), and that is load-bearing: 0 is the RE-PASS's mark. Pinned by
|
||||
// runs.TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn and its chapter-shaped twin.
|
||||
// The ORDER itself lives on the book, in units.
|
||||
OrderedChapters: quote.Chapters,
|
||||
Ceiling: quote.Hold,
|
||||
Now: s.now(),
|
||||
|
|
@ -486,9 +492,10 @@ func (s *Service) Start(ctx context.Context, in StartRequest) (pgstore.Run, erro
|
|||
// that is not — and the passes degrade rather than halt, which makes the difference invisible
|
||||
// in the book. The run carries what it really got.
|
||||
BondFunded: quote.BondFunded,
|
||||
// ⚠ SET ONLY FOR AN ORDER THAT DOES NOT CLOSE WHOLE CHAPTERS. It is what switches the run's bar
|
||||
// to units — see migration 00033: counted in chapters, such a run reads `0/N` for its whole
|
||||
// life, which is the state this very file refuses a book for at admission (PD-405).
|
||||
// ⚠ SET FOR EVERY ORDER PHRASED IN CHARACTERS — including one that happens to land on a chapter
|
||||
// boundary, see pricing.Quote.UnitShaped. It is what switches the run's bar to units: counted
|
||||
// in chapters, an order that stops inside one reads `0/N` for its whole life, which is the
|
||||
// state this very file refuses a book for at admission (PD-405). See migration 00033.
|
||||
OrderedUnits: unitShapedOrder(quote),
|
||||
Order: &order,
|
||||
}, offset, s.enqueue)
|
||||
|
|
@ -562,9 +569,12 @@ func (s *Service) resolveOrder(ctx context.Context, in StartRequest, priced pgst
|
|||
// ⛔ PRICED FROM THE UNITS THEMSELVES, not from the chapters they fall in. Quoting a unit
|
||||
// order at the price of every chapter it touches is what made this order meaningless exactly
|
||||
// where it is the only one available: a book with no chapter structure is ONE chapter, so a
|
||||
// thousand characters of it reserved the whole book. The chapter span below is carried only
|
||||
// for the RUN's bar, which is still counted in chapters (canon §Progress), and it is the
|
||||
// coarser of the two figures — a character order can stop inside a chapter.
|
||||
// thousand characters of it reserved the whole book. The chapter span below goes into the ROW
|
||||
// and no further: nothing publishes it any more (pgstore.runOrderedChapters nulls the view for
|
||||
// a unit-shaped run, because the span overstates what was bought), and the bar counts units
|
||||
// because `UnitShaped` is set two lines down. What the stored span is still FOR is the re-pass
|
||||
// discriminator — `ceiling_chapters = 0` is the re-pass's own mark, and a zero written here
|
||||
// would make this order look like one to three internal readers.
|
||||
q := s.Pricing.QuoteUnits(pb, balance, units, n, chaptersSpanning(pb, n))
|
||||
q.UnitShaped = true
|
||||
return q, pgstore.BookOrder{ThroughUnitID: units[n-1].ID}, nil
|
||||
|
|
@ -629,8 +639,11 @@ func (s *Service) ceilingFor(amount money.MicroUSD) ([]string, error) {
|
|||
return s.Cfg.Ceiling.Args(amount)
|
||||
}
|
||||
|
||||
// unitShapedOrder is the run's own volume when its order does not close whole chapters, and nil when
|
||||
// it does. Nil is the ordinary shape and leaves the bar counted in chapters exactly as before.
|
||||
// unitShapedOrder is the run's own volume when its order was phrased in CHARACTERS, and nil for every
|
||||
// other shape. Nil is the ordinary case and leaves the bar counted in chapters exactly as before.
|
||||
//
|
||||
// ⚠ The condition is the PHRASING (`q.UnitShaped`), not where the order landed — see
|
||||
// pricing.Quote.UnitShaped for why, and for the measurement that killed the older wording.
|
||||
func unitShapedOrder(q pricing.Quote) *int {
|
||||
if !q.UnitShaped || q.Units <= 0 {
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue