textmachine/frontend/docs/PROGRESS.md

28 KiB
Raw Blame History

Журнал зоны «Фронт»

Зонный журнал: сессии фронта пишут сюда сами, работают независимо, в 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 разойдутся — ровно то, ради чего строка заведена как ранняя вставка. Дальше — манифест глав и пофазный прогресс (строки 99100), к 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/» стояло в плане, а в коде компонент импортировал фикстуру напрямую — шва не было вовсе. Радиус поражения был мал (один экран), но образец для 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)
Сетевой гейт ловил только fetchEventSource (ратифицированный транспорт) проходил молча починено: восемь форм, все проверены живым нарушением
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.