252 lines
28 KiB
Markdown
252 lines
28 KiB
Markdown
# Журнал зоны «Фронт»
|
||
|
||
> Зонный журнал: сессии фронта пишут сюда сами, работают независимо, в `docs/PROGRESS.md`
|
||
> оркестратора не пишут (решение владельца 02.08). Хвосты — `BACKLOG.md`, план и замеры —
|
||
> `FRONTEND_PLAN.md`, ратифицированные пины — `STACK_DECISIONS.md`.
|
||
> Сверху — текущее состояние, ниже — хроника по сессиям.
|
||
|
||
## Текущее состояние
|
||
|
||
- **Пройдено:** S0 (план) и S1 (инструменты, скриншот-цикл, `tokens.css`, витрина).
|
||
- **Дальше:** S2 — оболочка трёх панелей и слой `src/ui/`.
|
||
- **Ждёт владельца:** контраст палитры (`BACKLOG.md` Ф-11) и два вопроса ниже — В-1 диск, В-2 копирайт.
|
||
- **Ждёт движка:** до S2 — ничего. **Вход в S3 — контракт API v0 (единый бэклог, строка 95,
|
||
`docs/architecture/14-api-contract.md`): файла нет, а без него моки и API разойдутся —
|
||
ровно то, ради чего строка заведена как ранняя вставка.** Дальше — манифест глав
|
||
и пофазный прогресс (строки 99–100), к S5 — машиночитаемая таблица подписи банка (строка 101).
|
||
- **Карта канона для фронт-сессии — `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 промта, а промт правит не фронт-сессия.
|
||
|
||
### 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`: библиотека книг, хранение исходников и экспортов), но платформы ещё нет ни строки. Фронт от этого не зависит (работает на моках), но **вопрос на движок/платформу, а не на фронт** | скоуп-решение и порядок работ |
|
||
| В-2 | **Авторское право на текст фикстур.** Витрина использует короткую выдержку из настоящего прогона (蛊真人). Правило проекта уже есть: `eval/.gitignore:4` держит копирайтные тексты вне git. Сейчас взят объём под проверку плотности и не больше; нужно ли убрать совсем и заменить синтетикой — решать владельцу | юридика |
|
||
|
||
## Хроника
|
||
|
||
### 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.
|