# Журнал зоны «Фронт» > Зонный журнал: сессии фронта пишут сюда сами, работают независимо, в `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 копирайт. - **Ждёт движка:** манифест глав и пофазный прогресс (единый бэклог, строки 99–100), к 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/`» стояло в плане, а в коде компонент импортировал фикстуру напрямую — шва не было вовсе. Радиус поражения был мал (один экран), но образец для S2–S7 задавался неправильный. Заведено: `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 — то есть ДО первого продуктового экрана (S4–S7), поэтому переписывать экраны и правда не придётся. Но четыре вызова витрины на 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, замерено дважды независимо: 70–140 мс на пересборку 2284 узлов) · мёртвый моноширинный шрифт в бандле (Ф-20) · ступень замечания различается только цветом полоски (Ф-21 — нужно слово, а слово продуктовое, его выбирает владелец) · два хвоста Ф-12 (группировка дерева — S4, разделение банка — S5). **Опровергнуто скептиками** (и это ценно ровно так же): «закрыть вкладку можно только мышью» — неверно, `Delete` работает, а отсутствие `aria-keyshortcuts` уже лежало строкой Ф-17 с причиной; «потеря текста в полях фильтра при сворачивании панели» — поле слева заглушка до S6, поле справа фильтр, а не введённые данные. **Замеры, которые стоит знать дальше:** бандл 545 КБ / 168 КБ gzip одним куском после установки RAC — строка Ф-18; резать его осмысленно после S4–S7. ### 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.