textmachine/frontend/docs/PROGRESS.md

382 lines
45 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.

# Журнал зоны «Фронт»
> Зонный журнал: сессии фронта пишут сюда сами, работают независимо, в `docs/PROGRESS.md`
> оркестратора не пишут (решение владельца 02.08). Хвосты — `BACKLOG.md`, план и замеры —
> `FRONTEND_PLAN.md`, ратифицированные пины — `STACK_DECISIONS.md`.
> Сверху — текущее состояние, ниже — хроника по сессиям.
## Текущее состояние
- **Пройдено:** S0 (план), S1 (инструменты, скриншот-цикл, `tokens.css`, витрина),
**S2 (оболочка трёх панелей, слой `src/ui/`, масштаб списков, хвосты гейтов)**.
- **Дальше: S3 — слой данных. ВХОД ЗАБЛОКИРОВАН СНАРУЖИ и это не изменилось:** контракта API v0
нет (`docs/architecture/14-api-contract.md` отсутствует, единый бэклог строка 95). S2 в него
не упиралась — она данных не читает; S3 без него писать нельзя. Что фронт обязан подать автору
контракта, собрано строкой Ф-14.
- **Ждёт владельца:** контраст палитры (`BACKLOG.md` Ф-11) и два вопроса ниже — В-1 диск,
В-2 копирайт.
- **Ждёт движка:** манифест глав и пофазный прогресс (единый бэклог, строки 99100),
к S5 — машиночитаемая таблица подписи банка (строка 101).
- `npm run check` зелёный (5 шагов, 62 теста), `npm run check:full` зелёный, гейт доступности
зелёный на обоих маршрутах. Гейтов стало больше: включён Ф-4 (отступы), закрыты Ф-9 и Ф-10.
- **Карта канона для фронт-сессии — `FRONTEND_PLAN.md` §0.1, транспорт — §0.2.** Заведены
после того, как две ошибки S1 пришли из непрочитанного канона; читать до кода, не после.
- `npm run check` зелёный (5 шагов, 31 тест, тип-осведомлённый линт), `npm run check:full`
зелёный, гейт доступности в скриншот-цикле зелёный.
- **Гейты стоят и на коммите:** 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`. Типы: `name|term|title|place|org|nickname|fullname`,
статусы подписи ровно три: `auto|draft|approved`, провенанс `seed|ruby|auto|mined`;
- пары — `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 | продуктовая формулировка, которую видит пользователь |
| В-2 | **Авторское право на текст фикстур.** Витрина использует короткую выдержку из настоящего прогона (蛊真人). Правило проекта уже есть: `eval/.gitignore:4` держит копирайтные тексты вне git. Сейчас взят объём под проверку плотности и не больше; нужно ли убрать совсем и заменить синтетикой — решать владельцу | юридика |
## Хроника
### 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.