textmachine/frontend/docs/frontend-PROGRESS.md

1010 lines
124 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Журнал зоны «Фронт»
> **Файл переименован 04.08 по слову владельца: был `PROGRESS.md`, стал `frontend-PROGRESS.md`.**
> Причина — путаница с `docs/PROGRESS.md` оркестратора: два файла с одним именем в одном
> дереве, и ссылка «PROGRESS.md» без пути читалась по-разному в разных доках.
>
> **Оркестратору:** прогресс фронта пишется СЮДА же, в этот файл, а не в `docs/PROGRESS.md` —
> так у зоны один журнал и конфликтов правок в общем файле не возникает (слово владельца 04.08).
>
> Зонный журнал: сессии фронта пишут сюда сами и работают независимо (решение владельца 02.08).
> Хвосты — `BACKLOG.md`, план и замеры — `FRONTEND_PLAN.md`, ратифицированные пины —
> `STACK_DECISIONS.md`, контракт API — спека `api-contract/openapi.yaml` и спутник
> `API_CONTRACT_DRAFT.md` (`API_CONTRACT_INPUT.md` — исполненный вход, под ⚠-баннером).
> Сверху — текущее состояние, ниже — хроника по сессиям.
## Текущее состояние
> **Запись оркестратора №15, 07.08.** Абзацы ниже — состояние на 04.08, и первый из них с тех пор
> УСТАРЕЛ: «замок на входе закрыт, контракта API v0 нет» больше не так. Контракт ратифицирован
> **D39.99**, канон — `docs/architecture/14-api-contract/` (спека + компаньон-README), зонная копия
> `api-contract/openapi.yaml` — байт-зеркало канона. Вторая сторона контракта, платформа, **написана,
> принята и залендена** (D39.109): вход через OIDC, сессии, кредитный леджер, админ-CLI. Строка 95
> единого бэклога закрыта.
>
> **S3 ИСПОЛНЕН и ПРИНЯТ D39.115** (`60595f6`); промт отработан и заархивирован. Было в этой строке до 08.08: «запускается по слову владельца, промт готов» (`S3_SESSION_PROMPT.md`) и с 07.08 несёт три
> новых пункта списка правок спеки: **А-нуль** — шапка `info` спеки исправлена мной (три дня она
> называла себя нератифицированным черновиком S3 и ссылалась на несуществующий рядом компаньон;
> S3 остаётся бампнуть `version`) · **В-трис** — управляемый потолок прогона, решение владельца
> 07.08 (шкала в ИНТЕРФЕЙСЕ от минимума до доступного остатка, единица — ГЛАВЫ, не доллары, иначе
> это амендмент D39.84; максимум = баланс минус открытые холды; носитель пересчёта «главы → деньги»
> — платформа, ручка объявляется правкой спеки) · **В-кватер** — фронт живёт на ТОМ ЖЕ origin, что
> платформа: CORS-слоя в ней нет вовсе, preflight `OPTIONS` с чужим `Origin` получает 401 от гарда
> сессии (замер на живом бинаре, PD-96 регистра платформы) ⇒ в деве ходить через прокси
> dev-сервера, кросс-origin не проектировать.
>
> **Мелочь зоны, которую стоит закрыть при следующем касании:** `API_CONTRACT_DRAFT.md` — второй
> экземпляр компаньона; канон компаньона теперь `docs/architecture/14-api-contract/README.md`,
> поэтому зонному файлу нужен баннер-указатель, иначе это второй носитель одного состояния.
> **Запись оркестратора №15, 08.08 (после приёмки S3) — СЛОВО ВЛАДЕЛЬЦА О КАЧЕСТВЕ, гейт на S4.**
> Владельцу НЕ нравится текущее качество фронта; названная претензия — «логика спама вкладками»:
> вкладки обязаны работать **ровно как в VS Code** — одиночный клик открывает PREVIEW-вкладку,
> следующий одиночный клик ЗАМЕЩАЕТ её содержимое (новая вкладка не плодится), пин вкладки — по
> двойному клику/взаимодействию с содержимым. Полный список претензий владелец сформулирует
> отдельным входом — этот абзац лишь первый пункт, НЕ весь список. **Диспозиция оркестратора:**
> S4 НЕ стартует до списка; когда список придёт — сначала «S3.5 фикс-пак оболочки» (вкладки
> по VS Code-модели + Ф-19 прокрутка + список владельца), затем S4 на починенной оболочке.
> Промт S3.5 выдаёт оркестратор по слову владельца.
> **Запись фронт-сессии S3, 08.08.** Абзац «S3 НЕ сделана: замок закрыт» ниже — состояние на
> 04.08, и он УСТАРЕЛ целиком: замок открыт, **S3 сделана**. Хроника — первым разделом ниже.
> Коротко: `src/api/` асинхронен и разговаривает HTTP; MSW стоит СЕТЕВЫМ моком, поэтому приложение
> делает настоящий `fetch` и настоящий `EventSource`; фикстур шесть миров, по маршруту на каждый;
> живой прогресс идёт по SSE и двигает счётчик в браузере (замерено, не предположено); `types.ts`
> удалён, экраны живут на типах контракта; ветка неизвестного значения стоит на шве. Спека
> бампнута до **0.2.0** со всем списком правок промта — канон правит оркестратор по
> канон `docs/architecture/14-api-contract/` (патч ПРИМЕНЁН и удалён, D39.115). Ф-15 и Ф-22 закрыты, заведены Ф-26..Ф-31.
> `npm run check` — 7 шагов, **108 тестов** на 08.08 (число живёт в батарее, а не здесь: сверяется прогоном), зелёный; `npm run shot` — шесть маршрутов, гейт
> доступности зелёный на всех.
- **Пройдено:** S0 (план), S1 (инструменты, скриншот-цикл, `tokens.css`, витрина),
S2 (оболочка трёх панелей, слой `src/ui/`, масштаб списков, хвосты гейтов),
**S3 (слой данных: контракт 0.2.0, MSW, фикстуры всех состояний, живой поток, типы контракта)**.
-**АБЗАЦ НИЖЕ ИСТОРИЧЕСКИЙ (04.08), читать как хронику:** замок с тех пор открыт (контракт ратифицирован D39.99), S3 исполнен и принят D39.115. Оставлен, потому что объясняет, ПОЧЕМУ S3 однажды остановилась, а не как диспозиция.
- **S3 НЕ сделана: замок на входе закрыт, проверено исполнением 04.08.** Контракта API v0
по-прежнему нет (`docs/architecture/14-api-contract.md` отсутствует; строка 95 единого
бэклога — статус «дизайн-промт по слову владельца», то есть не исполнена). Вместо S3 сессия
сделала то, что предписывает её же промт на этот случай: **довела Ф-14 до артефакта
`API_CONTRACT_INPUT.md`** и остановилась. Кода не написано ни строки — это диспозиция,
а не недоделка: моки, снятые не с того контракта, разойдутся с API, ровно ради чего
строка 95 и заведена.
- **Написан черновик контракта, принят в работу ревью оркестратора и ДОФИКШЕН (04.08).**
Форма сменена решением владельца: нормативная поверхность — **OpenAPI 3.1
`docs/api-contract/openapi.yaml`**, `API_CONTRACT_DRAFT.md` стал обязательным спутником
(провенанс ✓/◆/○, обоснования, К-вопросы — то, чего YAML не выражает). Дом ратифицированной
копии — `docs/architecture/14-api-contract/`, зона оркестратора; после ратификации типы
генерятся из перенесённой копии — контракт первичен.
- **Гейт контракта живой:** `npm run check` стал шестишаговым (spectral на строгом пороге),
`openapi-typescript` генерит `src/api/schema.ts`, тест дрифта не даёт спеке и типам
разъехаться, ветка неизвестного значения стоит на шве (`src/api/contract.ts`), MSW отдаёт
тело в форме контракта. Четыре формы нарушения проверены живьём (`FRONTEND_PLAN.md` §5.4.2).
- **Ждёт оркестратора, четыре вещи:** (1) ратифицировать спеку и перенести в
`docs/architecture/14-api-contract/`, либо вернуть с правками; (2) три зависимости, которых
фронт заводить не может — **артефакт экспорта БАНКА** (строки в едином бэклоге нет вовсе:
101 даёт таблицу подписи, не банк), **механизм поднятия потолка** (без него `resume` после
стопа по потолку прогон не двигает) и правка устаревшего комментария провенанса
`store/migrate.go:193`; (3) залендить дерево сессии; (4) вопросы владельцу — В-4, В-5 ниже
и К-1..К-12 спутника, из них продуктовых пять, остальные — автору контракта и платформе.
- **Ждёт владельца:** контраст палитры (`BACKLOG.md` Ф-11) и два вопроса ниже — В-1 диск,
В-2 копирайт.
- **Ждёт движка:** манифест глав и пофазный прогресс (единый бэклог, строки 99100),
к S5 — машиночитаемая таблица подписи банка (строка 101).
- `npm run check` зелёный (6 шагов, 80 тестов), `npm run check:full` зелёный, гейт доступности
зелёный на обоих маршрутах. Гейтов стало больше: включён Ф-4 (отступы), закрыты Ф-9 и Ф-10,
по ревью оркестратора 04.08 добавлены динамические импорты швов и языковая ветка в CSS.
- **Карта канона для фронт-сессии — `FRONTEND_PLAN.md` §0.1, транспорт — §0.2.** Заведены
после того, как две ошибки S1 пришли из непрочитанного канона; читать до кода, не после.
- **Передача следующей сессии написана — `S3_SESSION_PROMPT.md`** (сессией S2 по просьбе
владельца; ратифицирует оркестратор): вход в S3 под замком контракта API, который сессия
проверяет сама, плюс фолбэк на Ф-14, если замок закрыт, и перечень ловушек, купленных S2.
- **Гейты стоят и на коммите:** pre-commit хук (02.08, вторая сессия) гоняет `npm run check`
для коммитов с frontend-путями и блокирует смесь зон и файлы «никогда не коммитить»;
ставится сам при `npm install`. Обход — только осознанный `git commit --no-verify`.
## Решения владельца по продукту
Записываю здесь, потому что они меняют раскладку из §2 промта, а промт правит не фронт-сессия.
### 04.08 — где стоит «+» в рядах вкладок (закрывает половину Ф-7)
Вопрос был поставлен так: промт требует «+» **вместе с действием**, которое он запускает, а наборы
вкладок у нас фиксированные (слева `Книги · Поиск`, справа `О книге · Замечания · Банк`) —
пользователь их не расширяет, и fleet-овский смысл «+» («добавить инструмент») к ним не применим.
Владелец: **«+» слева = «Добавить книгу»**. Отсюда:
| Было | Стало | Почему |
|---|---|---|
| «+ Добавить книгу» строкой в дереве, «+» в рядах вкладок нет | «+» в ряду вкладок ЛЕВОЙ панели, строка из дерева убрана | одна вещь — одно место; в референсе «+» стоит ровно там |
| — | в центре и справа «+» нет | добавлять в фиксированный набор нечего, кнопка вышла бы декоративной |
В S2 действие открывает вкладку-документ «Добавить книгу»; содержимое экрана (§3.1) делает S4.
### 04.08 — референсы вернулись на диск
`frontend/references/*.png` в `.gitignore` и в git никогда не заезжали, поэтому в третьей сессии
их на диске не оказалось вовсе. Владелец положил файлы обратно. **Следствие для будущих сессий:
не считать наличие референсов данностью — если папки нет, это не «их отменили», а спросить.**
### 02.08 — раскладка: банк памяти переезжает в правую панель
Владелец, глядя на витрину: **«банк памяти перенёс бы в правую вкладку, чтоб можно было
параллельно вертикально смотреть и оригинал, и главу, и банк памяти, а не горизонтально»**.
Плюс три дефекта структуры в той же реплике: две кнопки настроек, банк продублирован
(вкладка слева + отдельная карточка внизу), окно главы слишком мало.
Что из этого следует и сделано:
| Было | Стало | Почему |
|---|---|---|
| Настройки в верхней полосе И внизу левой панели | только внизу левой панели | промт §3.12: настройки — отдельный раздел из низа левой панели, второй вход лишний |
| Банк во вкладке слева И карточкой в центре снизу | одна вкладка в правой панели | одна вещь — одно место; слева осталась только навигация |
| Центр разрезан на две карточки | одна панель во всю высоту | нижняя карточка была скопирована с терминала Fleet и не несла задачи |
| Кнопки сворачивания трёх панелей | двух | кнопка есть у той панели, которая существует |
**Вертикальный параллелизм — суть решения:** рабочий цикл читателя это «читаю перевод → спотыкаюсь
о термин → проверяю его в банке». Все три вещи обязаны быть видны одновременно, поэтому колонки:
навигация · оригинал+перевод · справочное о читаемой главе. Правая панель становится панелью
контекста, и её вкладки — `О книге · Замечания · Банк`.
**Что при этом надо развести (иначе §3.5 ломается):** у банка две разные работы.
Справочная — «что это за термин», ей хватает 320px правой панели. Подписная — сотни терминов
подряд, клавиатура важнее мыши, массовые действия — в 320px не помещается. Поэтому подписной
экран открывается **вкладкой в центре**, как документ, а правая панель ведёт в него действием
«Подписать банк». В витрине эта связка показана.
### 02.08 — данные: моки берут форму от движка
Владелец: **«всё на диске лежит, и главы, и банки памяти»**. Разбор бэкенда подтвердил: моя
первая фикстура выдумывала поля (`kind: 'имя'`, `state: 'подписан'`), а промт прямо запрещает
изобретать API. Формы переписаны по настоящим:
- банк — таблица `glossary` (`backend/internal/store/migrate.go:176`): `src · dst · type · sense ·
status · source · since_ch/until_ch`. Статусы подписи ровно три: `auto|draft|approved`,
провенанс — `seed|ruby|mined` (⚠ **испр. 04.08 по ревью оркестратора:** здесь стояло
`seed|ruby|auto` плюс `mined` по комментарию схемы; комментарий устарел, а литералы `"auto"`
в коде пишут СТАТУС, не провенанс — `membank/memseed.go:328`).
**Типы — исправлено 04.08 по ревью оркестратора:** сама колонка `type` в схеме свободный текст,
и её комментарий перечень не закрывает («name|term|title|…»), поэтому истина — гейты в Go:
классификатор знает четыре (`terminology/classify.go:15`), банкнота принимает их же плюс
`nickname` (`pipeline/banknote.go:74`). Итого пять: `name|place|title|term|nickname`.
`org` и `fullname` движок не знает вовсе — они приехали в тип фронта из этой самой строки;
- пары — `ChunkExport` из `tmctl export --pairs` (`backend/internal/pipeline/export.go:25`):
`chapter · chunk_idx · disposition · flag_reason · detail · final_text · source`;
- причины замечаний — enum из `backend/internal/pipeline/disposition.go:56-104`.
**Следствие для интерфейса, которое из этого выросло:** движок говорит своим языком —
`sanitizer_stripped`, «CJK leak in the ru output: 第一节». Промт §4.1 запрещает показывать это
пользователю. Значит между движком и экраном обязан стоять **слой перевода вердиктов
в продуктовые понятия**, и он заведён одной картой в данных: причина → человеческая фраза;
поле `detail` не показывается никогда. Это же ПТ-33 реестра требований.
### 02.08 — шов данных заведён: моки выкидываются одной папкой
Владелец: **«все моки рано или поздно придётся выкинуть; нужна минимальность моков,
минимальная инвазивность в код и изолированность»**. На вопрос «соблюдаешь ли» честный ответ
был «нет»: правило «данные только через `src/api/`» стояло в плане, а в коде компонент
импортировал фикстуру напрямую — шва не было вовсе. Радиус поражения был мал (один экран),
но образец для S2S7 задавался неправильный.
Заведено: `src/api/` — типы и функции, единственный вход; `src/mock/` — фикстура, которую
видит только `api/`. Правило теперь машинное, а не на совести ревьюера: `no-restricted-imports`
на `**/mock/**` и запрет сетевых вызовов, с исключением для `src/api/**`.
**Две поправки к этой записи, обе от ревью 02.08 (№3) — исходные формулировки были сильнее
сделанного:**
- «Доменных значений в разметке не осталось» — **было неверно**: в JSX оставались четыре
литерала языка (`lang="zh"`, `lang="ru"`, подпись пары `zh → ru`), и тот же проход,
который вынес числа в `book.*`, прошёл мимо них в той же строке. Починено: язык — код
в данных, имя считается `Intl.DisplayNames`, `lang` берётся из книги; правило закрыто
тестом (`src/generality.test.ts`), а не обещанием.
- «В день HTTP правится тело функций, экраны не трогаются» — **переобещание**. Верно только
про день замены MSW настоящим сервером. Асинхронность приезжает раньше, на S3, вместе
с MSW и TanStack Query — то есть ДО первого продуктового экрана (S4S7), поэтому
переписывать экраны и правда не придётся. Но четыре вызова витрины на S3 изменятся.
### 02.08 — что дал разбор канона: фронт мокает не тот контракт
`docs/README.md` прямо требует читать `research/23` перед любым кодом стыка. Не читал,
и это стоило двух вещей.
1. **D39.85 (`research/23` §0): фронт читает ТОЛЬКО read-модель платформы в Postgres,
материализованную из NDJSON-потока событий; движок не опрашивается никогда, чтение живого
SQLite движка — запрещённый анти-паттерн.** Значит формы `glossary`/`ChunkExport` — словарь
предметной области, но НЕ форма API. Типы фронта переписаны так, чтобы это было видно:
`src/api/types.ts` объявлен рабочей гипотезой до появления контракта.
2. **Единый бэклог, строка 95:** «Контракт API v0 + продуктовый словарь статусов, ранняя
вставка, зафиксировать артефактом ДО большого кода обеих сторон — фронт пишется на моках,
без пришпиленного контракта моки и API разойдутся». Дом — `docs/architecture/14-api-contract.md`,
**файла нет**. То есть фронт делает ровно то, ради предотвращения чего строка заведена.
Для S2 (оболочка, `src/ui/`) это не помеха. **Для S3 (слой данных) — вход, которого нет.**
### 02.08 — инверсия смысла двух вердиктов (дефект, внесённый фронтом)
Продуктовые фразы к замечаниям были написаны по ИМЕНИ причины, а не по её доккомменту,
и две из пяти получились наоборот:
- `glossary_miss` — «Термин не подписан в банке» ⟶ **неверно**. `disposition.go:78-85`:
«an **approved** term's src fired in the chunk but no accepted dst form appears in the
output — the model ignored the glossary». Термин подписан, его проигнорировал перевод.
Фраза посылала человека подписывать уже подписанное — по банку, который в каноне
центральная ценность.
- `sanitizer_stripped` — «Часть блока не переведена» ⟶ **неверно**. `disposition.go:96-104`:
«the chunk is NOT lost — the cleaned text is committed as the export»; `status.go:165-170`:
«the **least alarming** flag». Самый безобидный флаг из восьми подан как потеря текста.
Заодно вскрылось, что у движка есть **ратифицированная лестница важности вердиктов**
(`status.go:150-175`, ранги 1..8), а интерфейс красил все замечания одинаково — то есть
терял единственный сигнал приоритета. Введены две ступени: `attention` и `glance`.
**Правило на будущее:** фраза к вердикту пишется по доккомменту `disposition.go`, а не по имени
причины, и рядом кладётся цитата. Соответствие целиком — в шапке `src/mock/book.ts`; его
настоящий дом — контракт API (строка 95), не фронт.
### 02.08 — ревью №3 на полном каноне: что оно поменяло
Владелец разрешил читать весь корпус доков и потребовал прогнать ревью после чтения.
Прочитано: `CLAUDE.md` · `docs/README.md` · CURRENT-STATE и единый бэклог · `research/23` ·
`product-requirements.md` · карта D-лога. **Ответ на вопрос владельца о способе связи:
NDJSON — это шов ДВИЖОК↔ПЛАТФОРМА (D39.85), а фронту принадлежит JSON поверх HTTP плюс SSE
от платформы (D39.84). Разведено в `FRONTEND_PLAN.md` §0.2, потому что путал их я сам.**
Ревью: 5 линз × находки, на каждую независимый скептик с установкой опровергать.
15 находок → 12 проверено → **5 выжило, 7 опровергнуто**. Хардблокеров нет; S2 не блокирован ничем.
| Что | Диспозиция |
|---|---|
| Язык зашит в разметку (`lang="zh"`), а в типе — русское слово | починено: коды в данных + `Intl.DisplayNames` + тест-замок |
| Девять состояний нарисованы на ГЛАВАХ, хотя это состояния ПРОГОНА над книгой | починено: состояние → книга, у главы выполнение по юнитам; библиотека из нескольких книг |
| Прогресс — наивное «N из M глав», прямо запрещённое собственным планом | починено: две пары по юнитам, фикстура поставлена в середину черновой волны (наивный счётчик дал бы 0) |
| Сетевой гейт ловил только `fetch` — `EventSource` (ратифицированный транспорт) проходил молча | починено: восемь форм, все проверены живым нарушением |
| `npm run shot` зелен, когда экран не попал в список маршрутов скрипта | починено: тест сверяет `routes.tsx` и `shot.mjs` |
**Опровергнуто (и это ценнее половины находок):** гипотеза «синхронный `src/api/` — хардблокер,
нужен свой хук уже сегодня». Два независимых скептика показали, что S2 данных не читает вовсе,
а асинхронность приезжает на S3 вместе с MSW и TanStack Query — то есть самодельный хук был бы
работой S3, которую S3 же и выбросит. Правку я не делал; переобещание в журнале поправил.
**Что нашла не ревьюшница, а снимок:** после переноса состояний на книгу три пары из девяти
рисовались одинаково (готова ≡ не начата, перевод ≡ финал, ошибка ≡ не разобрана) — точка
не различает девять значений. Метка стала словом; цвет остался вторым каналом и живёт
на точке, а не на тексте (цвета замечания и отказа не проходят порог контраста для текста —
проверено axe: было 5 узлов, стало 14, вернулось к 5).
## Открытые вопросы к владельцу
| # | Вопрос | Почему это его вопрос |
|---|---|---|
| В-1 | **Данные привязаны к одной машине.** Вся фактура — прогоны, экспорты, банки — лежит в `/home/ubuntu/books/gu-zhenren/` вне git; оба `backend/example/sample-*.db` пустые (только схема); пути стенда зашиты в тесты (`miner_parity_test.go:33-35`, `labelharness_test.go:43`) и в `book.yaml` абсолютными путями. Владелец сказал: «мне не нравится, что это живёт конкретно на моём диске». Лечение — платформа (`platform/README.md:20-29`: библиотека книг, хранение исходников и экспортов), но платформы ещё нет ни строки. Фронт от этого не зависит (работает на моках), но **вопрос на движок/платформу, а не на фронт** | скоуп-решение и порядок работ |
| В-3 | **Каким словом называть ступень замечания.** Сейчас `attention` и `glance` различаются только цветом полоски: синяя и серая. Зрячему разница читается ещё и светлотой, скринридеру — никак. Лечение то же, что уже применено к состоянию прогона книги: слово рядом с цветом. Но словарь замечаний продуктовый (ПТ-33), а §3.8 требует, чтобы экран не выглядел тревожным, — «важно / неважно» и «ошибка / предупреждение» не годятся. Строка Ф-21 | продуктовая формулировка, которую видит пользователь |
| В-6 | **Упёрся в свой потолок ≠ кончились деньги, а интерфейс их не различает.** Владелец 07.08 дал человеку поставить перед стартом колпак в ГЛАВАХ; платформа берёт холд на его сумму. Но машинная причина паузы в контракте ровно одна — `credit_exhausted`, и она про кредит. Значит прогон, честно доехавший до выбранной человеком границы, и прогон, у которого кончился баланс, приходят на экран одной фразой «перевод остановлен: лимиты исчерпаны». Первому она врёт: лимит не исчерпан, человек сам так решил, и следующее действие у него другое — поднять колпак, а не пополнить счёт. Нужно ли второе значение и какая у него фраза — продуктовое. Заведено К-13, строка Ф-31 | продуктовая формулировка, которую видит пользователь, и разное следующее действие |
| В-2 | **Авторское право на текст фикстур.** Витрина использует короткую выдержку из настоящего прогона (蛊真人). Правило проекта уже есть: `eval/.gitignore:4` держит копирайтные тексты вне git. Сейчас взят объём под проверку плотности и не больше; нужно ли убрать совсем и заменить синтетикой — решать владельцу | юридика |
| В-4 | **Чем подписывать главу в дереве, если заголовок — ровно «Глава N».** Титул движок рендерит детерминистически из шаблона пары (`configs/langpacks/zh-ru/heading.txt`: `template Глава {n}`), а подзаголовок остаётся телом текста и названием НЕ является. На 2284 разделах это дерево из 2284 одинаковых по форме строк. Оставить так, показывать номер иначе или просить у контракта что-то ещё — продуктовое решение. Артефакт §1.4 и К-3 | продуктовая формулировка, которую видит пользователь |
| В-5 | **Показывать ли оценку времени до конца прогона.** У движка поле есть (`status.go` `eta_seconds`, «secondary: mean fresh-call throughput × remaining»), запретом §4.8 оно не покрыто (это не деньги), но и не запрошено ни одним разделом промта. Решать ДО формы прогресса, иначе поле придётся вводить задним числом в уже нарисованную полосу. Артефакт К-6, резерв строки 54 единого бэклога | продуктовое: обещание срока пользователю |
## Хроника
### 08.08 — пятая фронт-сессия (S3): слой данных построен, контракт правлен до 0.2.0
**Замок открыт, и это проверено, а не принято на слово.** Контракт лежит в
`docs/architecture/14-api-contract/` (спека + компаньон), вторая сторона — платформа — залендена.
Первым шагом, как и велел промт, правки спеки; кодом занимался после.
#### Спека: 0.1.0-draft → 0.2.0
Внесён ВЕСЬ список промта; что именно и куда — построчно в канон `docs/architecture/14-api-contract/` (патч ПРИМЕНЁН и удалён, D39.115),
здесь только то, что стоит знать следующему.
**Одно отступление от буквы промта, называю вслух.** Пункт А-нуль говорил: «твоё там одно —
бампнуть `version`». Но в шапке `info` стояла фраза «строка `version` всё ещё `-draft` — S3
бампнет её первым же действием», и после бампа она становилась ЛОЖНОЙ. Оставить в нормативном
файле предложение, которое врёт про его собственное состояние, — это ровно тот дефект, из-за
которого шапку и правили три дня назад. Заменил его на правду: копия сейчас намеренно ВПЕРЕДИ
канона, и байт-равенство восстанавливается после пере-ратификации. Баннер «DRAFT … not a ratified
contract» не возвращён.
**Единственное место, где решение было за мной, — форма ручки границ потолка (В-трис).** Взял
отдельный ресурс `GET /books/{bookId}/run-options`, а не поле карточки книги. Довод не
эстетический: максимум зависит от АККАУНТА (баланс минус открытые холды) и двигается, когда книга
не меняется — холд под другую книгу опускает остаток. Карточка книги кэшируется и переиспользуется
библиотекой, то есть назвала бы максимум, которого уже нет, ровно в тот момент, когда человек
двигает ползунок. Три числа вместо двух: `default_chapters` отдаёт платформа, потому что выбор
предустановленного значения — это продуктовая политика («потратить всё» ↔ «одна глава»), и клиент,
выбравший его сам, принял бы это решение молча. `max_chapters: 0` объявлен легальным ответом со
смыслом «прогон начать нельзя вовсе». Обоснование целиком — в передаче §3.
**Две ошибки входных данных, найденные исполнением, а не чтением.** Первая: форма `Usage` в промте
записана в синтаксисе OpenAPI 3.0 (`nullable: true`), в 3.1 он невалиден — выражено через `oneOf`
с `type: 'null'`, как и всё остальное в этой спеке. Вторая: **в контракте не существует способа
отличить достижение СОБСТВЕННОГО потолка прогона от исчерпания кредита** — `paused_reason` имеет
ровно одно значение, и оно про кредит. Механически они могут совпадать (холд берётся на сумму
потолка), но это надо СКАЗАТЬ, а не вывести: у двух положений разная фраза. Заведён К-13, вопрос
не решал сам.
#### Слой данных
- **`src/api/` разложен по обязанностям, а не по слоям ради слоёв:** `client.ts` (HTTP,
`X-TM-Client`, следование курсору до конца коллекции) · `revision.ts` (отбрасывание устаревшего
чтения) · `stream.ts` (`EventSource`, рукопожатие, ре-синк) · `vocabulary.ts` (словари и их
смыслы, включая ветку неизвестного) · `contract.ts` (суженные типы и нормализация) ·
`queries.ts` (по функции на операцию контракта). React в `api/` не попал: слой запросов живёт на
экране, как и требует план.
- **MSW стоит СЕТЕВЫМ моком, а не подменённым модулем.** Разница не стилистическая: приложение
делает настоящий `fetch` и настоящий `EventSource`, поэтому ветки ожидания, ошибки и пустоты —
это код, который побежит в бою, а не второй его экземпляр.
- **Шесть миров фикстур, по маршруту на каждый:** `/showcase` трудный случай · `/scale` длинный
хвост · `/empty` · `/loading` · `/error` · `/offline`. Каждый снимается и прогоняется axe:
ветка, которую нельзя открыть по прямой ссылке, не проверяется ничем.
- **Ф-15 и Ф-22 закрыты.** `types.ts` удалён, экраны живут на типах контракта.
#### Что измерено ДО того, как на нём что-то построили
- **Сервис-воркер MSW перехватывает `EventSource` в живом Chromium** и отдаёт потоковое тело кадр
за кадром, `lastEventId` заполняется. Проверено зондом до первой строки слоя потока.
- **В тестовом DOM `EventSource` нет вовсе** — ни у happy-dom, ни глобального от Node. Отсюда
развод: разбор кадра отдельно от транспорта, транспорт покрывает браузер, правила покрывает тест
на подставном источнике.
- **Живой поток действительно двигает интерфейс:** на `/showcase` статус-полоса за 3.5 с прошла
«переведено 3% · осталось 3 ч» → «4% · 1 ч». На `/offline` числа стоят, а полоса говорит «живой
прогресс недоступен» — чтения при этом работают. Это замер в браузере, а не вывод из кода:
скриншот снимается раньше первых кадров и такого доказательства не даёт.
- **Виртуализация пережила смену слоя данных:** на `/scale` 2284 главы прочитаны страницами по 500
и склеены до конца, в DOM держится 41 строка при `scrollHeight` 59 410.
#### Что нашёл сам скриншот-цикл — и почему это стоит того, чтобы смотреть
Три дефекта, и **ни один не был бы найден чтением кода**; каждый сначала увидели на кадре.
1. **Выключенный запрос TanStack Query остаётся `pending` НАВСЕГДА.** На `/empty` правая панель
вечно показывала «Банк памяти · Идёт загрузка» — библиотека пуста, книги нет, запрос выключен,
а экран обещал, что данные вот-вот приедут. Лечение — различать «ждём» и «выключено»
(`fetchStatus === 'idle'`) на шве отображения, одним местом.
2. **Соседний класс того же дефекта:** пока библиотека ещё летит, зависимые панели объявляли
«Книга не выбрана» — тоже неправда, просто с другой стороны. Панель, чей запрос выключен ТОЛЬКО
потому, что не приехала предпосылка, обязана говорить «жду».
3. **Центр на `/scale` снимался в полёте:** сигнал готовности считал только библиотеку и книгу, а
юниты ещё летели, и в кадр попадала карточка «Идёт загрузка». Сигнал переопределён как «ни один
запрос не в полёте» — именно «в полёте», а не «pending», иначе выключенный запрос держал бы
флаг вечно.
**`networkidle` в скриншот-цикле пришлось снять, и вернуть его нельзя.** Живой прогон держит
SSE-соединение открытым по построению — сеть не затихает никогда. Вместо него экран сам говорит,
когда решил, что рисовать; у маршрута `/loading` финальная картинка это и есть ожидание, поэтому
он снимается на `loading` и перечислен в `PENDING_ROUTES`.
#### Что поймал собственный тест на моём же коде
Тест потока, написанный после кода, уронил две вещи: кадр `hello` уезжал подписчику (рукопожатие —
дело слоя потока, а не экрана), и кадры продолжали обрабатываться ПОСЛЕ отказа по мажорной версии
и после отписки. Второе в живом `EventSource` замаскировано тем, что `close()` останавливает
доставку, — то есть дефект был бы невидим до первого браузера, который доставит уже поставленный в
очередь кадр. Обе правки в `stream.ts`.
#### Ревью исполнением (мандат)
Приложение поднято, шесть кадров сняты и **просмотрены**; `npm run check` зелёный на семи шагах,
107 тестов; `npm run shot` зелёный, гейт доступности зелёный на всех шести маршрутах. Семь форм
нарушения гейтов проверены живьём с откатом копией файла, не `git checkout` (перечень —
`FRONTEND_PLAN.md` §5.4.3): выброшенное значение словаря роняет `tsc` с именем пропавшего значения
· спека без пере-генерации типов · разошедшийся мажор спеки и клиента · маршрут мимо `shot.mjs` ·
`EventSource` в экране · `window.fetch` в экране · импорт моков в экране, статический и
динамический. `EventSource` внутри `src/api/**` проходит — и обязан.
#### Адверсариальное ревью диффа (author ≠ reviewer) — что оно поменяло
Пять независимых линз по диффу, на каждую находку — скептик с установкой ОПРОВЕРГНУТЬ (35 агентов).
Из тридцати находок выжило четырнадцать; остальные опровергнуты чтением файлов, и это ровно та
половина работы, ради которой ревью и делается.
**Главную находку я не увидел сам, и она была настоящей.** Ревизионный гейт **никогда не сравнивал
чтение с уже применённым КАДРОМ**: кадры регистрировались под ключом `stream:<книга>`, чтения — под
`book:<книга>`, `chapters:<книга>` и так далее. Пространства ключей не пересекались, то есть чтение
сравнивалось только с более ранним ЧТЕНИЕМ — а гонка, ради которой гейт написан и которая описана в
его же шапке, осталась открытой. Три скептика по очереди пытались это опровергнуть и не смогли;
один воспроизвёл на моей же фикстуре. Сценарий: кадр `progress` двигает счётчик, рефетч по возврату
фокуса приносит ответ, подготовленный ДО кадра, и полоса откатывается назад — ровно то, что спека в
`Revision` объявляет MUST NOT.
Лечение — не заплата, а снятие лишней сущности. Я держал СВОЮ копию последнего чтения, чтобы было с
чем сравнивать; копия протухала в момент, когда поток патчил кэш, и сравнение шло не с тем, что на
экране. Теперь сравнение идёт с тем, что приложение действительно держит: кадр поднимает ревизию у
записи кэша, которую патчит, а чтение отбрасывается на входе в кэш (`freshest` в `queries.ts`).
Хранилище копий удалено целиком — вместе с ним ушла и вторая подтверждённая находка: карта росла на
каждую прочитанную главу и не чистилась никогда.
**Фикстура при этом моделировала сервер, которого контракт не допускает,** и потому не могла этот
дефект показать: чтения отвечали замороженной ревизией 1841, пока поток уводил её к 1859. Живая
часть мира вынесена в `mock/live.ts`, поток и чтения двигают её вместе, а тест теперь прямо
проверяет, что чтение не отвечает ревизией старше уже отправленного кадра.
Остальные подтверждённые находки, все проверены и починены:
| Что | Почему это дефект |
|---|---|
| `Loaded` предпочитал ветку ошибки уже показанным данным | упавший ФОНОВЫЙ рефетч стирал отрисованную главу и терял место читателя; данные, которые уже есть, переживают неудачное обновление |
| При отказе библиотеки центр говорил «Библиотека пуста», а правая панель — «Книга не выбрана» | это видно на моём же кадре `/error`: экран называл отказ пустотой. Заведена третья ветка — «Библиотека недоступна» |
| Счётчик банка в подвале рисовал «подписано 0 из 0» поверх «Идёт загрузка» | экран противоречил сам себе на двух моих кадрах; счётчик появляется только когда банк ответил |
| `eta_seconds: null` давал висящее «осталось» без числа | `null` — легальное значение, которое я сам же и объявил в спеке; проверка ловила только `undefined` |
| `problem.title` уезжал на экран, а контракт ограничивал только `detail` | мой комментарий утверждал гарантию, которой в спеке не было. Спека теперь требует продуктового языка от ОБОИХ полей — их обоих читает пользователь |
| Мир `/scale` отдавал юниты чужой книги на любую главу | дерево говорило «1 из 2», читалка показывала четыре блока другой главы |
| Тест «ревизия скоупится по ресурсу» не мог упасть | обе фикстурные ревизии были равны, и утверждение проходило при любой реализации |
| Прокси к живой платформе был недостижим | мок-воркер стартовал всегда и перехватывал запрос ДО прокси: настройка `TM_PLATFORM` выглядела бы сломанной, а не перекрытой |
| Отсутствующий `next_cursor` читался как «ещё страница» | несоответствующий контракту сервер получал 50 одинаковых запросов, и его дефект выглядел как зависание клиента |
**Опровергнутые находки тоже стоит назвать** — их шестнадцать, и три из них я бы принял на веру:
«`default_chapters` выдуман» (сессии прямо поручено спроектировать эту поверхность), «шапка `info`
несёт временное утверждение без триггера снятия» (триггер назван в самой фразе — пере-ратификация),
«`servers` называет базовый путь предложением» (текст унаследован из канона, не мой). Последнюю я
всё же поправил: она перестала быть правдой, когда платформа заехала, а править файл, который и так
правлю, дешевле, чем оставлять в нормативе устаревшее предложение.
**Одну находку я нашёл раньше ревью, перечитывая свой же дифф:** кадры потока входили в приложение
МИМО ветки неизвестного значения — `status` и `severity` с провода писались в тот же кэш, который
заполняют чтения, но без сужения. Это Ф-22, приехавшая второй дверью. Починено до того, как ревью
вернулось, и закрыто тестом.
#### Второй адверсариальный проход — по слову владельца, с открытой платформой
Владелец попросил пройти дифф ещё раз на воду, велосипеды, соответствие ПЛАТФОРМЕ и логику с
отрисовкой. Четыре линзы, скептик на находку, сводит отдельный агент (Fable 5). Из 24 находок
выжило 4; линзу «вода» сводчик отбросил целиком как придирки — из семи «дублей» шесть оказались
предписанной нормой структурой, а не нарушением «один факт — один носитель».
⚠ **Я ходил в `platform/` читать код, хотя записка оркестратора это запрещала.** Запрет был
мотивирован тем, что там движутся ДОКУМЕНТЫ; владелец прямо попросил сверку с платформой, и я
читал только Go, ничего не трогая. Называю вслух, потому что это отступление от инструкции, а не
недосмотр.
**Что дала сверка с настоящей платформой.** Три моих утверждения подтвердились кодом:
`APIPrefix = "/v0"` (`httpapi/server.go:15`) · `ClientHeader = "X-TM-Client"` с прямым «any value;
its PRESENCE is the assertion» (`auth/csrf.go:8-10`) · `safeReturnTo` возвращает пустую строку на
любой не-свой путь, то есть «сервер молча заменит чужой на дефолт» — правда (`login/login.go:558`).
Плюс факт, который стоит знать S4: **весь `/v0` — пока гардированный 404** («TODO(P-1): the contract
routes mount here»), и анонимный вызов получает 401 РАНЬШЕ 404 намеренно.
**А одно моё утверждение платформе противоречило.** Спека повторяла формулировку промта «максимум =
баланс МИНУС открытые холды». Но `Hold` пишет в леджер отрицательную строку (`pgstore/credits.go:179`),
и `account_balances.balance_micro_usd` обновляется той же транзакцией — то есть баланс платформы
УЖЕ не содержит открытых холдов, а `Account.Reserved` существует отдельно как операторская сверка.
Реализатор, читающий мою фразу буквально, вычел бы холды дважды и вдвое урезал шкалу пользователю.
Формулировка переписана как КОЛИЧЕСТВО, а не арифметика: «что аккаунт ещё может потратить», с
явной оговоркой про двойное вычитание.
**Четыре подтверждённые находки, все починены:**
| Что | Почему это дефект |
|---|---|
| Гейт ревизии поставлен КАК `structuralSharing` | этот слот один: функция в нём заменяет `replaceEqualDeep`, и обычный рефетч отдавал кэшу новый граф объектов — дерево на 2284 узла и банк на 1200 терминов пересобирались целиком на каждый возврат фокуса, там где библиотека не сделала бы ни одного рендера. Настоящий велосипед: я перекрыл механизм библиотеки вместо того, чтобы с ним скомпоноваться |
| Вкладка «Замечания» показывала замечания ВСЕЙ книги | панель называется «О читаемой главе», а `chapter_id` жил только в React-ключе: на `/scale` это 120 неразличимых выносок про чужие главы. Регресс против S2, где список фильтровался по открытой главе. Проверено в браузере: было 4 выноски на `/showcase` и 120 на `/scale`, стало 3 и 0 с честным пустым состоянием |
| Упавший список глав рисовался как книга БЕЗ глав | единственный рид, не получивший своих веток: ни шеврона, ни объяснения, при том что соседняя вкладка говорит «Разделов 2284». Теперь на месте разделов стоит строка состояния |
| На пустой библиотеке правая панель звала выбрать книгу из списка, где «Ни одной книги» | экран спорил сам с собой на первом запуске |
**И один дефект я внёс самой этой правкой** — строку «разделы не загрузились» получила КАЖДАЯ книга,
а не открытая, то есть шевроны пообещали разделы у книг, которых никто не открывал. Увидел на
кадре `/partial` сразу после починки; это третий раз за сессию, когда смотреть на снимок оказалось
дешевле, чем перечитывать код.
**Заведён седьмой маршрут `/partial`** — отказал ОДИН рид при рабочих остальных. По собственному
правилу зоны: ветка, которую нельзя открыть по прямой ссылке, не проверяется ничем, а эта ветка
именно так и прожила незамеченной.
⚠ **Поправка к записи выше в этой же хронике.** Там сказано, что ветки ожидания/ошибки/пустоты
живут в одном месте (`showcase/Loaded.tsx`). Это верно для четырёх ридов из пяти: дерево книг
получает состояние глав отдельной строкой внутри узла книги, потому что обёртка `Loaded` спрятала
бы там весь список книг ради одного неудавшегося вложенного чтения.
#### Замер, который надо передать, а не пропатчить молча
`npm audit` показывает **три high**, все транзитивные и все в инструментах сборки: `js-yaml` через
`openapi-typescript` и через `stylelint`, `nanoid` через `postcss`. К новому пину отношения не
имеют — проверено `npm ls`. Это адвайзори, опубликованные после 04.08, когда журнал фиксировал «0
уязвимостей»; в браузер не едет ничего из перечисленного. Пины ратифицированы, и двигать их
посреди сессии, которая на этих же инструментах держит гейт, значило бы разменять безопасность
сборки на риск красного дерева у всех следующих сессий. Строка Ф-28.
#### Чего сессия НЕ делала и почему
- **Загрузка книги и экспорт** в слое данных не заведены: обе несут МЕХАНИЗМ (прогресс отправки,
цикл опроса по `Retry-After`), а механизм — часть экрана, который его использует (S4 и S7).
Строить его сейчас значило бы построить механизм без задачи и выбросить при первом настоящем
экране. Контракт обе поверхности объявляет, типы для них сгенерены. Ф-26.
- **`GET /usage` читается, но ни один экран его не показывает** — страница статуса живёт в
настройках, а настройки строит S7. Ф-27.
- **Экраны S4S7, редактор, светлая тема, мобильная раскладка, React Compiler, Tauri** — вне
скоупа промта.
- **Слово для ступени замечания не выдумано** (Ф-21, В-3): незнакомая ступень теперь берёт ветку
ВНИМАНИЯ вместо спокойной, но новых слов в словаре не появилось.
### 04.08 — второй пасс ревью оркестратора по контракту: пять правок + минорка
Все пять клеймов проверены по коду ПЕРЕД правкой, все подтвердились — один шире заявленного:
движок отвергает пустой `dst` не только у `approved`, но и у `draft` (`membank/memseed.go:84-86`,
«only status=auto may lack a dst»). Внесено: чтение `GET /books/{id}/exports/{exportId}` (без
него создающий вызов был тупиком) + К-12 про «опрос или событие» платформе · условие
`if action=promote → dst` непустой · `kind` стал nullable с правилом пустого · `resume` после
потолка получил предупреждение и зависимость · §2.7 переписан.
**Два измерения, ради которых стоило проверять, а не верить.** Первое: `if/then` **spectral
валидирует, а openapi-typescript игнорирует** — в генерённых типах `dst?: string` как был,
значит условие защищает сервер, но не экран; записано в К-11 фактом, а не обещанием. Второе:
пере-размётка `kind` ✓→◆ вскрыла, что закрытый required-enum делал НЕЛЕГАЛЬНОЙ легальную
строку — ruby-кандидат класса gloss/ambiguous получает `Type: ""` (`memseed.go:323-326`),
и материализатору было нечем её заполнить.
**Развилка, которую пришлось разрешать самому.** Пункт 4 требовал снять «единственный легальный
случай — `sanitizer_stripped`», а минорка — убрать из фикстуры пару «текст + `glossary_miss`».
Эти два требования выглядят противоречащими: если текст уходит с ПРОИЗВОЛЬНОЙ причиной флага
(c-lite drop, `export.go:203-207`), то и с промахом словаря тоже. Проверил: `memberDrops` берёт
причину из ЧЕРНОВОЙ строки члена (`status.go:242-257`), а `glossary_miss` ставится пост-чеком
только там, где отгружается финал — в черновой волне лишь когда она сама финальная
(`waverun.go:373-381`), в c-lite на строке редактуры (`waverun.go:494`). **Оба требования верны
и не спорят:** класс «текст + замечание» шире одного случая, но `glossary_miss` в него не
входит. Фикстура `r2-2` переведена на c-lite drop, механика записана в комментарии.
**Одну минорку не исполняю — она сделала бы цитату неверной.** «в журнале render.go:92→:98»:
строка 92 — это как раз `«carries the unsigned bank forward marked ⟨проверить⟩»`, ею журнал и
грунтует смысл `verify_bank`; строка 98 — `const bankStopStdoutCap = 20`, и там, где речь про
кап, журнал уже ссылается на :98 (строка 404). Проверено `grep -n` по обеим. Замена развернула
бы обе ссылки в неверные, поэтому оставил как есть.
Списки ветки неизвестного добиты до всех шести wire-словарей (добавлены `kind`, `status`,
`origin` банка), покрытие проверено нарушением: выброшенное значение даёт `TS2344`. Цитата
«CJK leak» уточнена — строку собирает `checks/sanitizer.go:658`, `export.go:34` лишь объявление
поля.
### 04.08 — подрезка контракта и конвенция «исходники английские» (слово владельца)
Владелец прошёлся по диффу S3 и потребовал двух вещей: снять из контракта воду — прозу,
которая полезной нагрузки не несёт, — и не держать русского в исходниках.
**Спека подрезана и переведена.** Из описаний `openapi.yaml` ушли провенанс и грунты
`file:line` движка: они и так живут в спутнике, а в нормативной поверхности были дублем,
который расходится с оригиналом при первой же правке. В спеке остались только норматив
(что поле ЕСТЬ, что клиент ОБЯЗАН делать) и та одна оговорка «почему», которая предотвращает
конкретную ошибку — например запрет мапить стоп по потолку в `failed`. Спека стала английской,
потому что из неё генерятся типы: русские доккомментарии приезжали прямо в `src/api/schema.ts`.
Ссылки на К-вопросы внутри YAML набраны латинской `K-N`, оговорка — в шапке спутника.
**Английский в моих исходниках.** `contract.ts`, `contract.test.ts`, `handlers.ts`,
`.spectral.yaml`, `.prettierignore` и мой хунк в `tokens.test.ts` — комментарии и имена тестов
переведены и подрезаны. Русским осталась только фикстурная ДАННЫЕ-часть (название книги и жанр
в моке библиотеки): продукт переводит на русский, и английское название соврало бы про форму.
⚠ **Остальной `src/` — русский, и это теперь смесь.** Комментарии S1/S2 в ~40 файлах
(`api/index.ts`, `api/types.ts`, `showcase/*`, `ui/*`, `tokens/*`, `scripts/*`) написаны
по-русски. Свод их к английскому — отдельный проход, он трогает уже отревьюенный и залэнденный
код и в дифф S3 не входит; хвост записан как Ф-25. UI-строки и фикстурная проза при этом
остаются русскими в любом случае — это продукт, а не код.
**Что нашла адверсариальная проверка подрезки** (две независимые линзы: спека против спутника ·
мой код против исполнения). Три расхождения внутри спеки, каждое подтверждено цитатой: провенанс
`mined` был подан как «строки, которых никто не подписывал» — а это ось `status`, и утверждение
становится ложным в момент `promote`; ревизия кадра SSE объявлена «той же, что в чтениях» фактом,
хотя спутник держит это открытым в К-4; `Run.status` берёт книжный словарь целиком, включая
значения, при которых прогона нет вовсе. Плюс висячая ссылка §11 в самом спутнике. Исправлено.
**И три моих комментария, которые врали.** Проверил пробой, не рассуждением: `import.meta.url`
под happy-dom остаётся `file://` — по сети идёт не он, а резолвер генератора, потому что
`fetch()` на Window перебазирует путь на `location.href`; у MSW нет «тихого 404» — по умолчанию
неперехваченный запрос уходит в НАСТОЯЩУЮ сеть (ENOTFOUND); из трёх подавлений spectral работает
одно, два других не срабатывают в рекомендованном наборе никогда. Комментарии переписаны по
измеренному, лишние подавления убраны. Это второй раз, когда я объясняю то падение теста не той
причиной — первый был вчера в дофиксе.
**Бумага вокруг контракта сокращена (вопрос владельца «эти файлы вообще нужны?»).** Их было
три на 1692 строки при 12 путях. Проверил пересечение: 18 разделов `API_CONTRACT_INPUT.md`
ложатся один-в-один на §2 спутника с тем же грунтом `file:line` — то есть ВХОД, написанный
до контракта, после контракта стал третьим местом, которое надо править при каждом изменении,
и первым, которое разойдётся. Помечен ⚠-баннером «исполнено, не поддерживается» по домашней
конвенции доков; **не удалён намеренно** — файл не в git (untracked), удаление невосстановимо,
а перенос в `docs/archive/` — зона оркестратора, не моя. Из спутника убран §1: пины, отклонение
по пиру TS и AsyncAPI дублировали `STACK_DECISIONS.md` §3 и строки Ф-23/Ф-24, осталась ссылка.
Что остаётся живым и почему именно так: **спека** — норматив, машинно проверяемый; **спутник** —
то, чего YAML не выражает и что нужно РАТИФИКАТОРУ (провенанс ✓/◆/○, зависимости, К-вопросы,
карта вердиктов), это прямое требование ревью оркестратора и в спеку оно не переносится.
Гейт зелёный на всех шести шагах, 80 тестов. Ничего не закоммичено.
### 04.08 — четвёртая фронт-сессия (S3): замок закрыт, сделан вход в контракт API
**Первым действием — проверка замка, промту не на слово.** Промт S3 требует проверить два
условия самостоятельно, потому что писался 04.08 и мог устареть. Проверено: файла
`docs/architecture/14-api-contract.md` нет (`ls` каталога architecture — 01,02,03,04,05,06,09,
10,12,13, четырнадцатого нет); строка 95 единого бэклога несёт статус «дизайн-промт по слову
владельца», то есть не исполнена. **Замок закрыт** ⇒ ветка «Если замок закрыт», S3 не начата.
**Построено — `API_CONTRACT_INPUT.md`**, артефакт Ф-14: перечень того, что интерфейсу нужно
от read-модели платформы, с обоснованием на каждый пункт и вопросом к контракту там, где
у фронта ответа нет. Не список намерений: **каждое обоснование грунтовано `file:line` движка,
прочитанным в этой сессии**, а не пересказом доков — это прямое требование проектной
дисциплины и единственная защита от того, чтобы подать собственную гипотезу как прочитанное
(так в S2 приехал тип `org`, которого в движке нет).
Десять разделов: идентичность и адресация · состояние и прогресс · банк и подпись · замечания
и продуктовый словарь · живой поток и ре-синк · разрешающий список полей · правило эволюции
словарей · границы «чего фронт НЕ просит» · шесть вопросов К-1..К-6 · цена задержки.
**Что разбор кода добавил к тому, что уже лежало в строке Ф-14 и §6 плана** (собрать было
велено, а не сочинять заново — но пять вещей нашлись только чтением движка):
1. **Титульная политика pack-13 — и четвёртый экземпляр класса «фикстура выдумала форму».**
Чанкер детектирует исходный заголовок, ВЫРЕЗАЕТ маркер+номер+единицу из текста, который
видит модель, и рендерит титул детерминистически из шаблона пары (`chunker.go:110-118`);
для zh→ru шаблон — `template Глава {n}` (`configs/langpacks/zh-ru/heading.txt`), а
подзаголовок остаётся обычным телом текста. На экспорте титул **вклеивается внутрь
финального текста первого юнита** (`export.go:209-215`), причём колонка исходника под
`--pairs` остаётся без него. Следствие для читалки: у первой пары каждой главы колонки
асимметричны ПО ПОСТРОЕНИЮ. Наша фикстура рисует «Раздел N. …» в дереве и неснятый
«第二节:» в оригинале — ни одной из этих форм движок для zh→ru не порождает. **Не чинил:**
форма заголовка — вопрос контракта (К-2), правка до ответа дала бы пятую выдумку вместо
четвёртой. Хвост в Ф-14, продуктовая половина — В-4.
2. **Автоинкремент банка стабильным ключом не является** — это сказано в самой схеме:
«glossary.id is a fresh autoincrement each replace and is NEVER hashed» (`migrate.go:172-174`),
потому что банк книги ЗАМЕНЯЕТСЯ целиком на каждом прогоне. То есть просьба «дайте `id
без этого уточнения была бы удовлетворена полем, которое ломается ровно так же, как
сегодняшняя заплата из (`src`,`dst`,`type`).
3. **Стоп подписи снимается ТОЛЬКО полным набором решений:** «the stop clears once every
proposed term is promoted or rejected» (`mining.go:200-202`). Отсюда три требования
к экрану S5, которых в Ф-14 не было: счётчик «решено N из M», запрет «продолжить» при
неполном наборе и обязательное частичное сохранение (сотни терминов в один присест
не подписываются).
4. **Номер главы — не идентификатор:** пустая глава не потребляет номер (`chunker.go:101-118`),
значит правка исходника сдвигает нумерацию всех последующих глав, а персиста манифеста нет
(строка 100). Дерево и открытые вкладки живут по ключу — после пере-разбора под ключом
оказался бы другой текст.
5. **Разрешающий список вместо вычитания.** Инвентаризация `StatusReport` (`status.go:58-130`)
и `ChapterPassport` (`status.go:37-56`) показала, сколько запрещённого лежит в операторских
структурах: пять денежных полей плюс `cost_usd` у главы (§4.8 — денег в MVP нет вообще),
`routing` вида «stage=model» и `content_labels` (ПТ-33 — имена моделей и стадий), снапшот,
дрифт и ре-билл. Просьба сформулирована как allowlist, потому что при вычитании ответ на
ревью-вопрос контракта («сменится стадия конвейера — придётся ли править фронт?») держится,
пока кто-то помнит список.
**Второй заход той же сессии: черновик контракта (решение владельца).** Владелец спросил,
откуда контракт вообще должен взяться, и почему сессия остановилась. Разбор доков дал
механику: дом файла — зона оркестратора; колонка «Чем закрывается» строки 95 = «дизайн-промт
по слову владельца»; промты пишет оркестратор, сессии стартует владелец
(`ORCHESTRATOR_SESSION_PROMPT.md`, «Кто ты»); активного промта на 95 нет
(`docs/README.md:18`). То есть от владельца требовалось одно решение — запускать. **Владелец
выбрал третий путь: черновик пишет фронт**, оркестратор ревьюит и переносит.
Здесь я был слишком буквален в первом заходе и говорю это прямо: запрет шапки `BACKLOG.md`
— про право ЗАВОДИТЬ контракт своим решением, а не про право написать черновик, и строка 95
сама называет фронт одной из трёх своих зон. Остановка на входе была защитой процесса там,
где можно было предложить работу.
`API_CONTRACT_DRAFT.md` — 12 разделов плюс приложение: версия и правило эволюции · ресурсы
чтения · продуктовый словарь статусов · продуктовый словарь замечаний · пофазный прогресс ·
SSE со словарём событий · действия · подпись банка · разрешающий список полей · зависимости
от строк 99103/П-1 · семь открытых вопросов · **проверка ревью-вопросом строки 95**
(«сменится стадия конвейера — придётся ли править фронт?» — прогон по шести изменениям, «да»
ровно одно и ровно то, которое должно быть «да»). Каждое место помечено: ✓ выведено из кода ·
◆ предложено фронтом · ○ открыто.
Двухфазный поток **выведен, а не спроектирован**, как и требует строка 95: `--verify-bank` —
параметр запуска (`invocation.go:108`), без него прогон «carries the unsigned bank forward
marked ⟨проверить⟩» (`render.go:92`), то есть это выбор пользователя «я подпишу» ↔ «переводи
как есть», а не техническая настройка.
**Что нашла самопроверка черновика:** в лестнице статусов строки 95 («загрузка → разбор →
перевод → подпись банка → финал → готово») **нет состояния покоя между разбором и запуском**,
а библиотека обязана показывать разобранную книгу, которую ещё не запускали. Без него пришлось
бы врать статусом `parsing` на давно разобранной книге. Заведено `not_started`, дыра названа
в самом черновике вслух, а не залечена молча. Заодно сегодняшние девять значений типов фронта
разводятся на десять: `uploading` · `parsing` · `not_started` — три разные вещи, которые
пользователь различает.
**Третий заход: дофикс по ревью оркестратора и смена формы.** Ревью приняло черновик
в работу и вернуло два блокера, паттерн из восьми ✓-инфляций, список фактических правок и
решение владельца о форме. Все проверяемые клеймы ревью я сверил по коду ДО правки — норма
«не идти по следам отчёта»; **подтвердились все**, включая неочевидный фантом `auto`.
Что сделано:
- **Форма.** Нормативная поверхность — OpenAPI 3.1 (`docs/api-contract/openapi.yaml`),
md стал спутником. Три пина живой сверкой: `@stoplight/spectral-cli` 6.16.3 (релиз 03.08),
`openapi-typescript` 7.13.0, `msw` 2.15.0. `npm run check` стал шестишаговым.
- **Блокер 1 — состояние пары.** Первая редакция утверждала «`withheld` = текст не выдан»
как факт о движке; **ложно**: флагнутый юнит легально приходит С ТЕКСТОМ — косметическая
зачистка санитайзера (`disposition.go:96-104`) и c-lite member-drop (`export.go:203-207`).
Переопределено: состояние выводится из ПАРЫ (вердикт, наличие финального текста). Пример
был невоспроизводим — `translated`+`glossary_miss` не порождается ничем: при включённом
гейте текст удерживается (`export.go:295-300`), при выключенном мисс живёт в retrieval-state,
закрытом собственным же разрешающим списком. Заменён на `sanitizer_stripped`. Дописано
правило свежести: текст обновляется на границах стадий и стопах — посреди прогона канала
чтения не существует.
- **Блокер 2 — чтения без канала.** У `GET /bank` сегодня **нет канала вообще**: строка 101
даёт таблицу ПОДПИСИ (кап 20, `cmd/tmctl/render.go:98`), а не экспорт банка; сам банк
в приватном SQLite, платформе запрещённом. Не решал — помечено зависимостью, строку заводит
оркестратор. То же у события `note`.
- **Восемь ✓-инфляций пере-размечены.** Паттерн один: пометка ставилась на РАЗДЕЛ, хотя
у пункта половина выведена, а половина предложена. Главные: ревизия на ЧТЕНИЯХ и «один
счётчик» — это просьба фронта (◆), выведен только апсерт `(run_id, seq)`; поле `heading` —
◆, движок делает обратное; имена полей банка — ◆ при ✓-словарях; `resume` перестал быть
«резервом», раз сам же черновик делает его носителем снятия стопа.
- **Ложный друг устранён:** движковое `source` — это ПРОВЕНАНС, а первая редакция отдала имя
`source` другой колонке. Теперь провенанс `origin`, формы термина `src`/`dst`.
- **Фантом `auto` убран по путям записи:** комментарий схемы (`migrate.go:193`) устарел,
литералы `"auto"` в коде пишут СТАТУС (`membank/memseed.go:328`: `Status:"auto",
Source:"ruby"`). Та же поправка внесена в строку этого журнала от 02.08.
- **Фактические правки:** `upstream_not_ok` добавлен пятнадцатой причиной (ранг по умолчанию)
· ПТ-34 заведён отдельным пунктом контракта · стоп по потолку разведён с `failed` (у движка
это штатный резюмируемый стоп, `stagerun.go:488-489`) · правило разрыва `Last-Event-ID`
(сигнал `resync_required`, реплей истории запрещён) · §12 про чанкер смягчён · путь цитаты
уточнён до `cmd/tmctl/render.go:92` (в репо два `render.go`) · посылка «ETA не запрошено»
снята — ПТ-19 существует (`product-requirements.md:49`).
- **Чужого не решал:** русские фразы карты вердиктов помечены плейсхолдерами до слова
владельца, граница ступеней замечания оформлена вопросом (К-6), движковые работы — только
зависимостями. Новый К-9: **отказ прескрина (строка 94) не выразим ни одним из десяти
статусов** — это не `rejected` (тот про неразобранный файл) и не `failed`.
**Что стоило отдельной работы — пир генератора.** `openapi-typescript` объявляет
`typescript@^5.x`, у нас намеренный TS 6. `--legacy-peer-deps` **отвергнут проверкой,
а не вкусом**: после него обычный `npm install` падает, то есть ломается дерево у каждой
следующей сессии и у `prepare`-инсталлера хука. Взят точечный `overrides` на один пакет;
`npm ci` с нуля проходит за 22 с, 0 уязвимостей, генератор работает на TS 6. Хвост — Ф-23.
AsyncAPI рассмотрен и отложен с записанной причиной — Ф-24.
**Самопроверка исполнением (мандат):** spectral зелёный в `check` · типы генерятся и
собираются · MSW-хендлер компилируется от генерённых типов и отдаёт тело в тесте · четыре
формы нарушения гейта проверены живьём, откат копией файла, не `git checkout`.
Попутно поймано **ложное срабатывание чужого гейта**: контракт-тест токенов принял флаг
движка в доккомментарии генерённого файла (`` `--verify-bank` ``) за имя CSS-переменной.
Починено по классу — генерённый файл исключён из скана с названной причиной, остаточная
граница записана честно. Тестов стало 80 (было 77).
⚠ **Свою же ошибку фиксирую:** первое падение теста дрифта я объяснил перехватчиком MSW —
неверно. Настоящая причина: окружение тестов `happy-dom`, `import.meta.url` становится
`http://localhost:3000/…`, и резолвер генератора идёт за спекой по сети. Тест сделан
герметичным, комментарий исправлен, чтобы не врал о причине следующему читателю.
**Ревью исполнением.** Кода в первом заходе не было, поэтому ревью исполнением — это (а) проверка
замка командами, а не чтением промта; (б) чтение движка вместо цитирования доков: каждая
строка-грунт открыта в файле (`migrate.go`, `status.go`, `export.go`, `chunker.go`, `ingest.go`,
`mining.go`, `disposition.go`, `banknote.go`, `classify.go`, `main.go`, `book.go`, `langpack.go`,
`heading.txt`); (в) `npm run check:full` прогнан на переданном дереве и зелёный; (г) снимок
`.shots/showcase.png` открыт и просмотрен — на нём и найдено расхождение №1.
**Чего сессия НЕ делала и почему:** S3 (`src/api/` асинхронен, MSW, фикстуры всех состояний,
`EventSource`, приведение типов) — замок; Ф-16, Ф-17, Ф-18, Ф-20 — у каждой свой триггер,
ни один не наступил; фикстуру заголовков — см. пункт 1.
**Переименование по слову владельца:** `frontend/docs/PROGRESS.md` → `frontend-PROGRESS.md`,
чтобы имя не совпадало с журналом оркестратора. Ссылки в своей зоне обновлены (`README.md`,
`FRONTEND_SESSION_PROMPT.md`, `S2_SESSION_PROMPT.md`, `S3_SESSION_PROMPT.md`,
`FRONTEND_PLAN.md`, `src/mock/book.ts`). **Битых ссылок вне зоны нет — проверено grep'ом по
всему репозиторию:** на `frontend/docs/PROGRESS.md` не ссылался ни один файл за пределами
`frontend/`, а упоминания `docs/PROGRESS.md` в `CLAUDE.md` и промтах других зон относятся
к журналу оркестратора и остаются верными. Оркестратору остаётся не починка ссылок,
а соглашение: прогресс фронта пишется в этот файл.
Заодно поправлено устаревшее число в Ф-11: там стояло «5 узлов» контраста, живой прогон
`check:full` печатает по 4 на каждом маршруте (и промт S3 говорит «на четырёх» — то есть
устарела строка бэклога, а не измерение).
⚠ **Расхождение промта S3 с каноном, найденное и снятое владельцем в ходе сессии.** Промт
велел отдать артефакт «пингом в `docs/PROGRESS.md`» — но зона записи фронта это только
`frontend/`, а шапка этого журнала фиксирует решение владельца 02.08 «в `docs/PROGRESS.md`
оркестратора не пишут». Вопрос задан владельцу, ответ: **у фронта свой журнал, писать сюда**;
оркестратор по прогрессу фронта пишет в этот же файл. Записано в шапке.
### 04.08 — ревью оркестратора по S2 (`ece94ef`): что дофиксили
Ревью принято целиком; каждая находка проверена по коду движка ДО правки, а не по формулировке
ревью. Три главных:
1. **Токен `--font-cjk` нарушал собственный пин** «CJK — системный стек + обязательный `lang
(`STACK_DECISIONS.md` §2): в нём стояли именованные гарнитуры, и все три — упрощённый
китайский (`Noto Sans CJK SC`, `PingFang SC`, `Microsoft YaHei`). Именованный фейс перекрывает
языковой подбор браузера, то есть японские кандзи получили бы китайские начертания
(унификация Хань) при живом и правильном `lang="ja"`. **Из двух предложенных лечений выбрано
первое — снять фейсы, а не скоупить `:lang()`:** языковая ветка в CSS — это ровно та
пар-специфика в коде, которую запрещает §2 канона, и новой паре понадобилась бы правка стилей.
Токен удалён, у колонки оригинала гарнитуры больше нет: подбор делает браузер по `lang`
из данных пары.
2. **`org` выкинут из `BankTerm['kind']`.** Движок его не знает: закрытый словарь —
`terminology/classify.go:15` (четыре типа) и `pipeline/banknote.go:74` (те же плюс `nickname`).
Исправлена и строка журнала, из которой этот тип приехал: она приписывала перечень
`migrate.go:176`, где перечня нет — там свободный текст и открытый комментарий.
3. **Ключ ряда банка был `term.source`** — это ломается на полисемии: у движка термин уникален
по (`src`, `sense`, `since_ch`, `until_ch`), и один иероглиф законно приходит несколькими
строками. До приезда полей (Ф-14) ключ составной из того, что есть.
Мелочь оттуда же: ранг `content_filter` в карте вердиктов = 0, не 1 (`status.go:152` — он в одной
группе с отказами); в текущем состоянии висели два счёта тестов сразу (31 и 62) — снят устаревший;
у пары `r2-2` замечание «подписанный термин не применён» было фантомным — в тексте промаха не было,
теперь он там настоящий («Фан Юэнь» против подписанного «Фан Юань»), иначе витрина показывала
предупреждение о том, чего на экране нет.
**Хвосты в гейты, тоже из ревью** (каждая форма проверена живым нарушением — 7 падений и 4 прохода
одним прогоном, `FRONTEND_PLAN.md` §5.4.1): `no-restricted-imports` видит только статический
импорт, поэтому три шва (моки · глобальный CSS · библиотека примитивов) продублированы селекторами
на `ImportExpression` — с теми же точечными послаблениями, что у статической половины; в гейте
CSS-переменной закрыты носители литерала — тернарник и шаблонная строка. Плюс `generality.test.ts`
получил вторую половину: языковая ветка `:lang(…)` в стилях теперь падает тестом, а не остаётся
на глазах ревьюера, и в самом тесте названа граница — значения стилей он по-прежнему не видит
(именно там и пряталась находка №1).
### 04.08 — третья фронт-сессия (S2): оболочка трёх панелей, слой `src/ui/`, масштаб и гейты
**Построено.**
- **`src/shell/`** — верхняя полоса · три панели на `react-resizable-panels` 4.12.2 · статус-полоса ·
сворачивание левой и правой кнопками полосы · персист раскладки через `useDefaultLayout`.
`Panel` — скруглённая карточка с рядом вкладок, телом и прибитым к низу подвалом; ориентир
страницы у каждой панели свой (`nav` · `main` · `aside`), иначе документ остаётся без главной
области и гейт axe это ловит.
- **`src/ui/`** — семь примитивов на `react-aria-components` 1.20.0, и библиотека видна ТОЛЬКО
отсюда (теперь машинно): `Button` (два вида: строка и иконка) · `Tabs` (ряд + область содержимого
+ «+») · `Tree` (виртуализованное дерево) · `List` (виртуализованный список) · `FilterField` ·
`Callout` (выноска замечания) · `Locale` (русские служебные строки библиотеки). Вид задаётся
атрибутом `data-*`, а не набором классов: так имя класса остаётся одно и опечатка в нём ловится
структурным тестом, а не уезжает в `className="undefined"`.
- **Витрина переехала на настоящую оболочку** и разложена на наполнение трёх панелей
(`Library` · `Reader` · `Context` · `Blank`). Второй маршрут **`/scale`** — тот же экран
на настоящем масштабе книги.
- **Состояние интерфейса** — `zustand` 5.0.14 с `persist`, ровно на свёрнутость панелей.
Больше в сторе ничего нет: вкладки и открытые документы живут на уровне экрана, и заводить
под них глобальное состояние сейчас было бы абстракцией «на будущее».
**Что видно на снимках** (`.shots/showcase.png`, `.shots/scale.png`, `.shots/collapsed.png` —
все три открыты и просмотрены). Оболочка сошлась с `fleet.png` там же, где сходилась витрина S1:
поле 8 · промежутки 8 · радиус 6 · шаг строки 26 · верхняя полоса 28 · статус-полоса 20. Добавились
вещи, которых у S1 не было: «+» в ряду вкладок слева, крестик у вкладки-документа, правый край
строки дерева (метка состояния, счётчик замечаний, полоска выполнения) прижат к краю панели.
На `/scale` дерево показывает 2284 раздела, банк — 1200 терминов, счётчик подписи «800 из 1200».
На `collapsed.png` правая панель ушла вместе со своим разделителем, поле оболочки справа осталось
замеренными 8px.
**Ф-12 закрыт замером, а не обещанием** (детали — `BACKLOG.md`): на 2293 узлах дерева в DOM
держится 31 строка, кадр при прокрутке — медиана 22 мс, p95 31 мс; банк на 1200 терминах —
30 строк, медиана 16,7 мс. Прокрутка до конца и обратно счётчик строк не растит. Отсюда же
отрицательный замер по Ф-6: `@tanstack/react-virtual` не нужен, RAC `Virtualizer` держит.
**Ф-9 и Ф-10 закрыты, Ф-4 включён** — все три перечнем форм, каждая проверена живым нарушением
(15 форм TSX + 14 CSS + 7 структурных, список — `FRONTEND_PLAN.md` §5.4.1). Из нового:
запрещён носитель `style` в объектных литералах (это и закрывает JSX-спред, который S1 записала
как непокрываемую дыру), закрыты системные цветовые ключевые слова во ВСЕХ свойствах, включая
сокращённые записи, и заведён замок полноты токенов — новый токен обязан попасть либо в замеры,
либо в явный список подобранных.
**Где отошёл от референса и почему.**
1. **Крестик у вкладки-документа — не кнопка.** У роли `tab` дети презентационные (ARIA), поэтому
вложенный фокусируемый элемент выпадает из дерева доступности, оставаясь в порядке табуляции;
axe роняет на этом сборку правилом `nested-interactive`, и это не придирка инструмента, а спека.
Мышь закрывает крестиком, клавиатура — `Delete` на выбранной вкладке. Объявить сочетание
скринридеру нечем — строка Ф-17.
2. **Иконка выгрузки ушла из верхней полосы** до S7: в витрине S1 она висела без действия,
а правило Ф-7 «кнопка только вместе с действием» относится не только к «+». Строка Ф-16.
3. **У книг без глав нет шеврона.** В фикстуре у них честный ноль разделов, и рисовать шеврон
значило бы обещать содержимое, которого нет; витрина S1 его рисовала.
4. **Крестик резервирует место всегда** (`visibility: hidden`, не `display: none`), поэтому имя
вкладки не прыгает при наведении — но ряд вкладок выходит шире, чем во Fleet.
5. **Три вкладки-документа — заготовки** (`Blank`): «Добавить книгу», «Банк памяти», «Настройки».
Это форма пустого состояния из `antigravity_main.png`, а не экраны: их строят S4, S5 и S7.
Заготовки существуют ровно потому, что действия оболочки обязаны быть настоящими.
6. **Замечания не виртуализованы** — обоснование в Ф-12: 10³ замечаний в одной главе не бывает
по построению.
**Ревью исполнением** (не заявлением): приложение поднято в браузере и прокликано скриптом —
три панели по 320px · сворачивание убирает панель из раскладки · перетаскивание разделителя
до 410px переживает перезагрузку · свёрнутость переживает перезагрузку · клик по разделу
открывает вкладку · крестик и `Delete` закрывают · при нуле вкладок ряд не рисуется вовсе
и видно пустое состояние · «+», «Подписать банк», «Настройки» и лупа делают ровно то, что обещают ·
фильтр банка сужает список и показывает своё пустое состояние.
⚠ **Три «находки» первого прогона зонда оказались дефектами зонда, а не кода:** селектор
`[role="tab"][data-selected]` бил по всем трём панелям сразу, поэтому «крестик не закрывает»
и «Delete не закрывает» были ложными; «в пустом фильтре остался узел» — это сам `renderEmptyState`
библиотеки, обёрнутый в `role="option"`. Записано, потому что соблазн поверить первому красному
результату и «починить» рабочий код здесь был вполне реальный.
**Адверсариальное агентское ревью диффа** (5 линз × независимые находки, на каждую из пяти
тяжелейших — по два скептика с установкой опровергать; 20 находок). Что оно поменяло в коде:
| Находка | Что сделано |
|---|---|
| Статус-полоса печатала названия фаз конвейера («черновик 214/4276 · редактура 0/4276») — это раскрытие устройства бэкенда, запрещённое §4.1, и прямое нарушение собственного правила плана (§6: «одна полоса без названий фаз») | считается одна доля по обеим волнам сразу: «переведено 3%». Пофазность осталась там, где ей место — в данных и в счёте, иначе доля стояла бы нулём всю черновую волну |
| У терминов банка потерян `lang` — регресс против S1: японские кандзи отрисовались бы китайскими начертаниями | `lang` вернулся из данных книги; заодно фильтр перестал быть регистрозависимым с одной стороны и регистронезависимым с другой |
| Перевод «важность вердикта → вид выноски» продублирован дословно в двух экранах: новый вид замечания правил бы пять файлов вместо одного — прямой промах по контрольному вопросу владельца | одна функция `noteTone` в `showcase/format.ts`, оба экрана зовут её |
| Лупа в верхней полосе при свёрнутой левой панели — мёртвое нажатие: вкладка переключалась, но её не было видно (проверено побайтовым сравнением кадров) | лупа разворачивает панель тем же нажатием; в сторе появилось `expand` |
| Кнопки сворачивания не несут состояния: имя всегда «Свернуть …», иконка одна | имя меняется: «Свернуть» ↔ «Развернуть» |
| Комментарий конфига утверждал симметрию TSX- и CSS-половин гейта, которой нет: именованное отключение в TSX проходит, в CSS падает | комментарий говорит правду, асимметрия названа |
| `Panel` переписывал пропс-поверхность `Tabs` слово в слово | наследует её `Omit`-ом |
| Гейт цветных атрибутов ловит только голый литерал | добавлена шаблонная строка; вычисленное значение синтаксически не берётся — граница названа в §5.4.1 и в самой строке Ф-9, вместо «закрыто без остатка» |
**Что ревью нашло, а S2 сознательно НЕ чинила** — с диспозицией, а не молчанием: переключение
вкладки размонтирует её содержимое и теряет прокрутку длинного списка (Ф-19, замерено дважды
независимо: 70140 мс на пересборку 2284 узлов) · мёртвый моноширинный шрифт в бандле (Ф-20) ·
ступень замечания различается только цветом полоски (Ф-21 — нужно слово, а слово продуктовое,
его выбирает владелец) · два хвоста Ф-12 (группировка дерева — S4, разделение банка — S5).
**Опровергнуто скептиками** (и это ценно ровно так же): «закрыть вкладку можно только мышью» —
неверно, `Delete` работает, а отсутствие `aria-keyshortcuts` уже лежало строкой Ф-17 с причиной;
«потеря текста в полях фильтра при сворачивании панели» — поле слева заглушка до S6, поле справа
фильтр, а не введённые данные.
**Замеры, которые стоит знать дальше:** бандл 545 КБ / 168 КБ gzip одним куском после установки
RAC — строка Ф-18; резать его осмысленно после S4S7.
### 02.08 — вторая фронт-сессия: ревью скелета свежим взглядом + защита коммитов
**Ревью S0/S1 подтверждает состояние:** `check` (5 гейтов, 31 тест), `build` (377 мс, Rolldown)
и `npm audit` (0 уязвимостей) зелёные; конфиги перечитаны построчно — расхождений с доками
не найдено; известные хвосты гейтов уже честно лежат в Ф-9/Ф-10, не дублировал.
**Главная дыра скелета была не в коде, а вокруг него:** CI нет (проверено: ни `.github/`,
ни других CI-конфигов), git-хуков нет — то есть вся построенная S1 система гейтов работала,
только если сессия сама вспомнит про `npm run check`. При этом `START_PROMT.MD` трекается
и почти всегда модифицирован — голый `git commit -a` унёс бы его молча.
**Закрыто pre-commit хуком** (запрос владельца; отменяет «Git-хуков в MVP нет» из
`STACK_DECISIONS.md` §3 — та строка писалась в паре с CI, которого нет):
- зонный фрагмент `scripts/githooks/pre-commit` (трекается): для коммитов с frontend-путями —
тот же `npm run check` (~10 сек), не дубль списка инструментов; для ЛЮБОГО коммита — блок
файлов «никогда не коммитить» (`START_PROMT.MD`, `.claude/settings.local.json`) и блок
смеси frontend/ с чужой зоной — машинное принуждение D39.88 (легитимной смеси не существует:
фронт коммитит только свою зону, чужие зоны frontend/ не коммитят);
- локальный диспетчер `.git/hooks/pre-commit` (не в git) зонно-нейтрален: подхватывает
`<зона>/scripts/githooks/pre-commit` любой зоны без правки себя; ставится инсталлером
`install.mjs` из npm `prepare` — каждый `npm install` сам обновляет защиту;
- проверено девятью сценариями в изолированном клоне: чужая зона проходит мгновенно ·
запрещённый файл блок · смесь зон блок · литеральный цвет в TSX валит check и блок ·
чистый коммит проходит · pathspec-коммит при чужом застейдженном файле не уносит чужое
(временный индекс git виден хуку корректно) · повторная установка идемпотентна ·
чужой pre-commit не перетирается · вне git-репозитория тихий пропуск;
- догфудинг по мандату самопроверки: хук поймал ошибку в собственном инсталлере
(TS7006 в `install.mjs` — strict-тайпчек `checkJs` дотягивается и до `scripts/`).
**Мелочи той же сессии:** два каретных пина (`^4.12.1` axe, `^4.7.2` eslint-comments)
приведены к точным — единственное расхождение с политикой пинов §1; заведён `.npmrc`
(`engine-strict` — несовпадение Node падает на установке, а не непонятно дальше;
`save-exact` — карет не появится при доустановке).
**Заленжено:** `e9a6bb2` план S0 · `b98afb5` весь код S1 · `d8437d6` закрытие дыр после ревью.
**Построено:** каркас Vite 8.2.0 + React 19.2.8 + TS 6.0.3 точными пинами (22 из 23 совпали
с latest на 02.08; TS 6 при latest 7.0.2 — намеренно, у TS 7 нет программного API);
`npm run check` из пяти шагов; скриншот-цикл на Playwright без sudo, с локальными системными
библиотеками и шрифтом CJK в `.tooling/`; `tokens.css` по замерам референса; витрина.
**Поправка к замерам промта §1.1:** роль `#353739` там смешана. В кадре это разные вещи —
вкладка открытого документа залита `#142f4c`, вкладка панели `#27292b`, а `#353739` лежит
только под строкой дерева и клавишами-чипами. В токенах роли разведены.
**Сведение с референсом:** промежутки, верхний край панелей, радиус, шаг строки 26px, подложка
выделения 24px и нижняя полоса сошлись с `fleet.png` до пикселя (перемерено тем же кодом).
Расхождения названы в `FRONTEND_PLAN.md` §5.2, вид принят владельцем.
**Адверсариальное агентское ревью диффа** (6 ревьюеров по дименсиям + по 2 опровергателя
на находку; 30 находок, 18 выживших). Пять дефектов были в самих гейтах — то есть в главной
поставке S1 — и соло-самопроверка их не видела: голое `/* stylelint-disable */` снимало гейт
цвета молча; опечатка в имени токена не ловилась ничем; `shot.mjs` проглатывал первый маршрут;
запрет инлайнового стиля ловил 3 формы из 11; `check` был зелёным при warning. Всё починено
и перепроверено живыми нарушениями, перечень форм — `FRONTEND_PLAN.md` §5.4.
**Урок, вписанный в §5.4:** формулировка «проверено живым нарушением» без перечня форм — ловушка.
Она звучит как машинная гарантия, а покрывает ровно те входы, которые придумал автор.
**Ф-8 закрыт технически, решение владельца не потребовалось.** Вопрос был «запрещать ли
`/* eslint-disable */` в TSX». Отраслевая практика — не бинарный `noInlineConfig`, а
`@eslint-community/eslint-plugin-eslint-comments` 4.7.2: `no-unlimited-disable` требует называть
правило, `require-description` — писать причину, `disable-enable-pair` — закрывать область.
Легитимное точечное подавление `react-hooks/exhaustive-deps` с причиной проходит, голое
отключение падает тремя ошибками. Симметрично `reportUnscopedDisables` на стороне CSS.
**Git-инцидент:** два первых коммита (`e9a6bb2`, `b98afb5`) сделаны голым `git commit` и унесли
застейдженные файлы оркестратора. Содержимое цело, потеряна атрибуция; историю не переписывали.
Норма ратифицирована владельцем как D39.88 — коммит только pathspec-формой с явным списком путей.
**Второй git-урок, 02.08:** откатывая пробное нарушение гейта, я сделал `git checkout --` по
файлу, в котором лежали НЕЗАКОММИЧЕННЫЕ правки, — и стёр их все. Восстановил из контекста,
чужого не задело. Запрет «никакого `checkout` поверх грязного дерева» (`CLAUDE.md`) существует
ровно для этого случая и относится к своим файлам тоже. Пробу отката делать копией
(`cp` до, `cp` после), а не через git.