From 8b6f107cfad3ce39039eefb75a41944bb5c85b6b Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 4 Aug 2026 03:31:54 +0300 Subject: [PATCH] Add S3 handoff prompt gated on the API contract with F-14 fallback, plus new backlog row for unknown-value branches in closed unions --- frontend/docs/BACKLOG.md | 1 + frontend/docs/PROGRESS.md | 3 + frontend/docs/S3_SESSION_PROMPT.md | 179 +++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 frontend/docs/S3_SESSION_PROMPT.md diff --git a/frontend/docs/BACKLOG.md b/frontend/docs/BACKLOG.md index 28701944..4c6f7426 100644 --- a/frontend/docs/BACKLOG.md +++ b/frontend/docs/BACKLOG.md @@ -21,6 +21,7 @@ | Ф-17 | **Закрытие вкладки с клавиатуры не объявлено скринридеру.** Крестик у вкладки-документа сознательно НЕ кнопка: у роли `tab` дети презентационные (ARIA), вложенный фокусируемый элемент выпал бы из дерева доступности, оставшись в порядке табуляции (это же ловит axe правилом `nested-interactive`). Мышь закрывает крестиком, клавиатура — `Delete` на выбранной вкладке. Объявить сочетание нечем: `aria-keyshortcuts` не входит в типизированный API `Tab`/`TabList` в react-aria-components 1.20.0. Пересмотреть при обновлении библиотеки либо при появлении палитры команд | триггер: мажор RAC или экран горячих клавиш | S2 | | Ф-19 | **Переключение вкладки размонтирует её содержимое, и прокрутка длинного списка теряется.** Нашло адверсариальное ревью S2 и подтвердило независимым прогоном: `react-aria-components` не рендерит невыбранную `TabPanel` (`dist/private/Tabs.mjs:276` — `if (!isSelected && !shouldForceMount) return null`), поэтому на `/scale` прокрутка дерева `scrollTop=9000` → вкладка «Поиск» → обратно даёт `scrollTop=0`, а пересборка коллекции из 2284 узлов стоит 70–140 мс (два независимых замера). Ф-12 это не переоткрывает: там речь про поведение списка при 10³, и оно держится. **Чинить сейчас не стали:** `shouldForceMount` не лечит (панель остаётся видимым `div` и легла бы поверх соседней в той же ячейке грида), а настоящее лекарство — keep-alive либо запоминание офсета на вкладку — это решение об устройстве оболочки, и принимать его вслепую, до настоящих экранов, значит строить механизм не под задачу | S4 (экран глав) либо раньше, если владелец увидит потерю места | адверсариальное ревью S2 | | Ф-20 | **Мёртвый шрифт и мёртвые токены в поставке.** `@fontsource-variable/jetbrains-mono` грузится в бандл, но `--font-mono` не выбран ни одним правилом: художественную прозу моноширинным не набирают, а во Fleet на этом месте был код. Туда же несколько токенов, которые ни разу не используются (`--color-current-line`, `--color-hint`). Замок полноты токенов (S2) требует классифицировать токен, но не требует его использовать. Решать на S6, когда читалка станет настоящей: либо моноширинный находит работу, либо импорт и токены уходят | S6 | адверсариальное ревью S2 | +| Ф-22 | **У закрытых юнионов нет ветки неизвестного значения, и тайпчек здесь не страхует.** `runStates[book.state]` (`showcase/Library.tsx:103`) — обращение к `Record` по КОНЕЧНОМУ юниону, а `noUncheckedIndexedAccess` добавляет `undefined` только индексным сигнатурам: проверено пробой `tsc` — обращение по юниону компилируется молча, ругается лишь `Record`. Значение вне юниона (новое состояние прогона из контракта API) даёт `undefined.tone`, то есть падение дерева библиотеки, а не пустую метку; тем же классом `noteTone` тихо уводит незнакомую ступень замечания в спокойную ветку. Лечение — ветка неизвестного значения НА ШВЕ `src/api/`, где значение входит, а не в каждом компоненте. В S2 находку отвели скептики формулировкой «это работа S3» — она и есть работа S3, но хвост записан здесь, чтобы не жить только в промте | S3 (вместе с приведением типов к контракту) | адверсариальное ревью S2 → ревью оркестратора 04.08 | | Ф-21 | **Ступень замечания различается только цветом полоски** (`attention` синяя, `glance` серая). Зрячему разница читается ещё и светлотой, но скринридеру приоритет не достаётся вовсе. Лечение — слово, как это уже сделано для состояния прогона книги; **какое именно слово, решает владелец**: словарь замечаний продуктовый (ПТ-33), а §3.8 требует, чтобы экран не выглядел тревожным, так что «важно/неважно» не годится | решение владельца | адверсариальное ревью S2 | | Ф-18 | **Бандл 545 КБ (168 КБ gzip) одним куском** — замер 04.08 после установки `react-aria-components`; Rolldown предупреждает про порог 500 КБ. Для десктопного SPA за логином это не авария (индексации нет, сеть локальная), но одним куском едут и экраны, которых пользователь ещё не открыл. Лечение — разбиение по маршрутам, и делать его осмысленно после S4–S7, когда экраны появятся: сейчас резать нечего | после S7 либо при жалобе на первый экран | S2, замер | | Ф-5 | **Tauri 2.x вторым шагом** — по явным триггерам (трей · глобальные горячие клавиши · распространяемый .exe · офлайн · хранилище учёток ОС); до того — установимое PWA, только браузерные API | триггеры названы | STACK_DECISIONS §6, D39.84 | diff --git a/frontend/docs/PROGRESS.md b/frontend/docs/PROGRESS.md index b1f25c48..118d6e2a 100644 --- a/frontend/docs/PROGRESS.md +++ b/frontend/docs/PROGRESS.md @@ -22,6 +22,9 @@ по ревью оркестратора 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`. diff --git a/frontend/docs/S3_SESSION_PROMPT.md b/frontend/docs/S3_SESSION_PROMPT.md new file mode 100644 index 00000000..6c8986a0 --- /dev/null +++ b/frontend/docs/S3_SESSION_PROMPT.md @@ -0,0 +1,179 @@ +# Промт: фронт-сессия S3 — слой данных (вход под замком) + +Ты — фронтенд-сессия TextMachine, четвёртая по счёту. Зона записи — **только `frontend/`**; +сессия не коммитит — лендит оркестратор (протокол — стоящий промт `FRONTEND_SESSION_PROMPT.md` +§«Зона и git», он в силе целиком). Пре-коммит хук стоит и самоустанавливается при `npm install`. + +> Промт написан сессией S2 по просьбе владельца — как передача с той точки, где S2 встала. +> Ратифицирует и правит его оркестратор; при расхождении с его хендофф-промтом побеждает тот. + +## Входное состояние (04.08, проверено исполнением, а не заявлено) + +- **Пройдено S0–S2:** план · инструменты и скриншот-цикл · `tokens.css` по замерам референса · + оболочка трёх панелей (`src/shell/`) · примитивы `src/ui/` на `react-aria-components` · + витрина `/showcase` и нагрузочный `/scale` (2284 раздела, 1200 терминов). +- `npm run check` — 5 гейтов (prettier · eslint тип-осведомлённый · stylelint · tsc · vitest, + **77 тестов**), `npm run check:full` — + сборка + три скриншота с гейтом доступности axe. + Всё зелёное на 04.08. +- **Данные всё ещё синхронная фикстура:** `src/api/` отдаёт объекты из `src/mock/` напрямую. + Это диспозиция Ф-15, а не недосмотр: асинхронность приезжает вместе с MSW, то есть в S3. +- **Гейты, которые ты застаёшь:** одно место для цвета и размера (TSX-половина в + `eslint.config.js`, CSS-половина в `stylelint.config.js`) · три шва — фикстуры видит только + `src/api/`, глобальный CSS только `src/main.tsx`, `react-aria-components` только `src/ui/` — + закрытые и для статического, и для динамического импорта · сеть запрещена вне `src/api/` + во всех транспортах (`fetch`/`EventSource`/`WebSocket`/`XMLHttpRequest`, включая + `window.*`) · тест общности: код языка не зашивается в разметку, стили не ветвятся по + `:lang(…)` · контракт-тест токенов и структурные тесты CSS-модулей. +- ⚠ **На момент написания промта в дереве лежали незакоммиченные правки** по ревью оркестратора + (`frontend/src/{api/types.ts,generality.test.ts,mock/book.ts,mock/scale.ts,showcase/Context.tsx, + showcase/Reader.module.css,tokens/tokens.css,tokens/tokens.test.ts}`, `eslint.config.js`, + `frontend/docs/*`). Их лендит оркестратор. **Первым делом сверься с `git log` и `git status`:** + если правок в дереве нет — они уже уехали, ничего восстанавливать не нужно. + +## Замок на входе — первое действие сессии + +**Проверь сам, промту не верь** (он написан 04.08 и мог устареть): + +1. Существует ли `docs/architecture/14-api-contract.md`? +2. Что говорит строка **95** единого бэклога в `docs/PROGRESS.md` (контракт API v0 + продуктовый + словарь статусов) — она всё ещё «дизайн-промт по слову владельца» или уже исполнена? + +**Открыт** (контракт есть и ратифицирован) → скоуп S3 ниже. +**Закрыт** (на 04.08 — закрыт: файла нет) → раздел «Если замок закрыт». Не начинай S3 «пока +на своих типах, потом поправим»: моки, снятые не с того контракта, разойдутся с API — ровно +то, ради чего строка 95 заведена. + +## Обязательное чтение до кода (порядок) + +1. `frontend/docs/FRONTEND_SESSION_PROMPT.md` — стоящий промт: сценарий §3, жёсткие ограничения + §4, «как работать» §7. **Его не редактировать.** ⚠ Раскладка §2 частично устарела — её + перевешивают решения владельца из журнала зоны. +2. `frontend/docs/PROGRESS.md` целиком: «Текущее состояние» · «Решения владельца по продукту» · + «Открытые вопросы» · хронику 04.08 (в ней — что S2 построила, где отошла от референса и что + поправило ревью оркестратора). +3. `frontend/docs/FRONTEND_PLAN.md` — §0.1 (карта канона: что читать перед чем), **§0.2 + (транспорт: с движком фронт не разговаривает никогда — это ратифицированный анти-паттерн, + D39.81/D39.85)**, §5.4 и §5.4.1 (формы обхода гейтов и названные границы), **§6 (что уже + известно про движок и как это меняет форму данных — читать до первой строки типов)**. +4. `frontend/docs/STACK_DECISIONS.md` — §2 пины (там уже названы `@tanstack/react-query` и `msw`), + §5 транспорт и правила стрима, §7 риск ре-ингеста. Библиотеки сам не выбирай и версии + по памяти не «обновляй»: твои знания об экосистеме устарели, сверка — live по npm. +5. `docs/research/23-engine-platform-seam.md` — `docs/README.md` требует читать его **перед любым + кодом стыка**; форма событий и правило ре-синка оттуда. +6. `frontend/docs/BACKLOG.md` — строки **Ф-14** (вход фронта в контракт), **Ф-15** (закрывается + этой сессией), Ф-19, Ф-21. +7. `references/*.png` и `.shots/*.png` — открой и посмотри до первой правки. + +## Скоуп S3 — слой данных, и ни шагом дальше + +1. **`src/api/` становится асинхронным**: MSW как сетевой мок + серверное состояние на пинах + из STACK_DECISIONS §2 (версии сверить live на дату сессии; `.npmrc` держит `save-exact`). + Каждый новый пин — строкой в таблицу `FRONTEND_PLAN.md` с датой релиза и «зачем нам». +2. **Фикстуры всех состояний, а не одного счастливого:** ожидание · ошибка · пусто · + частично · длинный хвост · потеря соединения на стриме. Правило S1/S2, которое здесь + действует сильнее всего: **фикстура по умолчанию — трудный случай.** Наивный счётчик глав + должен на ней давать ноль (идёт черновая волна), иначе скриншот-цикл смотрит на удобное. +3. **Живой прогресс — `EventSource` за швом `src/api/`** (STACK_DECISIONS §5; гейт сети уже + запрещает его где-либо ещё). Докачка по `Last-Event-ID`/ревизии — часть контракта, не выдумка. +4. **Ф-15 закрывается здесь:** ветки ожидания/ошибки/пустоты появляются вместе с MSW, а не + после первого продуктового экрана. +5. **Типы `src/api/types.ts` приводятся к контракту.** Что в них сегодня заведомо временное: + булев `signed` вместо трёхзначного `auto|draft|approved` · ключ строки банка, собранный + из (`src`, `dst`, `type`), — у движка термин уникален по (`src`, `sense`, `since_ch`, + `until_ch`) · отсутствие ревизии для докачки. Всё это перечислено строкой Ф-14. + **Закрытые юнионы получают явную ветку неизвестного значения** (строка Ф-22) — сегодня её нет + ни у одного, и строгость тайпчека здесь НЕ страхует: `runStates[book.state]` + (`showcase/Library.tsx:103`) — обращение к `Record` по КОНЕЧНОМУ юниону, а + `noUncheckedIndexedAccess` добавляет `undefined` только индексным сигнатурам (проверено `tsc` + на пробе: обращение по юниону компилируется молча, ругается только `Record`). + Значит состояние прогона из будущего контракта даёт `undefined.tone` — падение дерева + библиотеки, а не пустую метку; `noteTone` тем же классом тихо уводит незнакомую ступень + замечания в СПОКОЙНУЮ ветку. **Место ветки — шов `src/api/`, где значение входит**, а не + каждый компонент: перевод «вердикт → вид» уже расползался по экранам, и контрольный вопрос + владельца требует, чтобы новое значение правило один файл. +6. **Поля не выдумывать.** Нет в контракте — это вопрос владельцу/оркестратору в + «Открытые вопросы» журнала зоны, а не догадка в типе. Дефект «фикстура выдумала поле» + в этой зоне случался дважды и оба раза стоил экрана. + +**Не в скоупе:** экраны S4–S7 · редактор текста · светлая тема · мобильная раскладка · +React Compiler (Ф-2) · Tauri (Ф-5). + +## Если замок закрыт + +Это ожидаемое состояние на 04.08, и правильный ход — не ждать молча и не строить экраны +на синхронных моках (порядок этапов Ф-1 стоит именно на слое данных). + +- **Доведи Ф-14 до артефакта, который можно отдать автору контракта:** не список намерений, + а перечень с обоснованием на каждый пункт — язык кодами · состояние прогона отдельно от + выполнения главы · прогресс пофазно по ЮНИТАМ · ревизия/`Last-Event-ID` · трёхзначный статус + подписи · поля банка (`type`/`sense`/`since_ch`) и ключ строки · продуктовый словарь вердиктов + (ПТ-33; §4.1 запрещает протечку внутренностей конвейера на экран). Половина уже написана + в строке Ф-14 и в §6 плана — собери, а не сочиняй заново. +- Отдай его оркестратору пингом в `docs/PROGRESS.md` и **остановись**: заводить контракт своим + решением фронт не может (шапка `BACKLOG.md`). +- Ф-16, Ф-17, Ф-18, Ф-20 — **не трогать**: у каждой в бэклоге стоит свой триггер, и ни один + из них не наступил. + +## Ловушки, купленные S2 — не переоткрывать + +- **`react-aria-components`:** `Tab` не принимает клавиатурных обработчиков (обработчик живёт + на ряду вкладок) · у роли `tab` дети презентационные, поэтому крестик закрытия — не кнопка + (axe роняет `nested-interactive`), закрытие с клавиатуры — `Delete`, объявить его нечем (Ф-17) · + **невыбранная `TabPanel` не рендерится вовсе** (`dist/private/Tabs.mjs:276`), отсюда потеря + прокрутки при переключении вкладки (Ф-19, замерено дважды) · `Tree` — это `treegrid`/`row`, + а не `treeitem`; раскрытие — `