Close S2 review findings: pair-agnostic source font via lang, bank kinds match engine vocabulary, composite bank row keys, dynamic-import seams and literal-carrier selectors gated, generality test covers CSS
This commit is contained in:
parent
062ad27d14
commit
02872b6cc1
14 changed files with 180 additions and 42 deletions
|
|
@ -13,7 +13,7 @@
|
|||
| ~~Ф-10~~ | ~~**Хвосты структурных тестов**~~ — **закрыты 04.08 в S2.** `cssModules.test.ts` строит пары по ИМПОРТАМ, а не по совпадению имён файлов, видит деструктуризацию `const { name } = styles` и выкидывает `url(...)` и строковые литералы до поиска классов; проверка сиротства стала «модуль, который никто не импортирует». `tokens.test.ts` получил замок полноты: каждый токен обязан попасть либо в замеры, либо в явный список подобранных, и обратно — список не может ссылаться на исчезнувший токен. Плюс новое: числа для виртуализатора живут в `src/tokens/measures.ts` и сверяются с одноимёнными токенами. Семь живых нарушений, перечень — `FRONTEND_PLAN.md` §5.4 | закрыт | адверсариальное ревью S1 |
|
||||
| Ф-11 | **Контраст ниже порога WCAG на 5 узлах** (приглушённый текст `#707479` и `#8a8e91` на `#17191a`). Это прямое следствие копирования палитры Fleet, которую владелец принял; axe отчитывается о них отдельно и команду не валит. Решение владельца: оставить как есть ради верности референсу или поднять приглушённые тона | решение владельца | ревью S1 №2 |
|
||||
| Ф-12 | **Масштаб списков** — **главное закрыто 04.08 в S2 замером, а не обещанием.** Заведён маршрут `/scale`: та же оболочка на настоящем масштабе — 2284 раздела и банк на 1200 терминов (`src/mock/scale.ts` через `getScaleFixture()`). Дерево и банк виртуализованы по умолчанию (RAC `Virtualizer` + `ListLayout`), и это измерено в живом браузере: на 2293 узлах дерева в DOM держится **31 строка**, `scrollHeight` 59 618px, кадр при прокрутке — медиана 22 мс, p95 31 мс; банк на 1200 терминах — 30 строк в DOM, медиана 16,7 мс. Прокрутка до конца и обратно счётчик строк не растит. **Замечания намеренно НЕ виртуализованы:** их список пер-главный, а юнитов на главу ≈1,9 (ПТ-21) и флаг у юнита один — 10³ там не бывает по построению; сводный вид по книге, если он появится, придёт с S6 и получит тот же примитив `ui/List`. **Два хвоста строки НЕ сделаны и получают диспозицию, а не зачёркивание** (нашло адверсариальное ревью S2): дерево осталось плоским без группировки по сотням — это работа экрана глав (S4), у оболочки для неё нет ни данных, ни требования; банк без разделения «подписано / ждёт решения» — это работа подписного экрана (S5), справочному виду в 320px разделение только мешает | масштаб закрыт; группировка — S4, разделение банка — S5 | ревью S1 №2 |
|
||||
| Ф-14 | **Вход фронта в контракт API (единый бэклог, строка 95)** — собрать одним списком то, что фронт уже знает и обязан подать автору контракта: язык кодами (не именами) · состояние прогона отдельно от выполнения главы · прогресс пофазно по ЮНИТАМ · ревизия/`Last-Event-ID` для докачки SSE · трёхзначный статус подписи термина (`auto|draft|approved`), а не булев `signed` · поля банка, без которых подпись не работает (`type`/`sense`/`since_ch`). **Своим решением фронт это не заводит** (`BACKLOG.md` шапка: запросы к движку идут строками единого бэклога через оркестратора) — строка держит список готовым к дизайн-промту 95 | к дизайн-промту 95 (до S3) | ревью S1 №3 |
|
||||
| Ф-14 | **Вход фронта в контракт API (единый бэклог, строка 95)** — собрать одним списком то, что фронт уже знает и обязан подать автору контракта: язык кодами (не именами) · состояние прогона отдельно от выполнения главы · прогресс пофазно по ЮНИТАМ · ревизия/`Last-Event-ID` для докачки SSE · трёхзначный статус подписи термина (`auto|draft|approved`), а не булев `signed` · поля банка, без которых подпись не работает (`type`/`sense`/`since_ch`) · **ключ строки банка** (ревью оркестратора 04.08): у движка термин уникален по (`src`, `sense`, `since_ch`, `until_ch`), то есть один и тот же `src` законно приходит несколькими строками, а список на экране требует различимого ключа — сейчас он собран из (`src`, `dst`, `type`) и это заплата, а не решение. **Своим решением фронт это не заводит** (`BACKLOG.md` шапка: запросы к движку идут строками единого бэклога через оркестратора) — строка держит список готовым к дизайн-промту 95 | к дизайн-промту 95 (до S3) | ревью S1 №3 |
|
||||
| Ф-15 | **`src/api/` синхронен, состояний загрузки и ошибки в типах нет** — диспозиция: НЕ чинить сейчас. Два независимых скептика показали, что S2 данных не читает вовсе, а асинхронность приезжает на S3 вместе с MSW и TanStack Query, до первого продуктового экрана; самодельный хук сегодня = работа S3, которую S3 выбросит. Строка держит обязательство S3: ветки ожидания/ошибки/пустоты появляются вместе с MSW, а не после первого экрана | S3 | ревью S1 №3 |
|
||||
| Ф-13 | **`eslint-plugin-jsx-a11y` не встаёт на ESLint 10** (peer `^3..^9`, проверено `--dry-run` → ERESOLVE). Статическую половину доступности держит axe в скриншот-цикле; плагин подключить, когда выйдет совместимый мажор | триггер: мажор плагина | ревью S1 №2 |
|
||||
| Ф-7 | **Хвосты сведения витрины с референсом** (`FRONTEND_PLAN.md` §5.2). **Часть S2 закрыта 04.08:** `+` стоит в ряду вкладок ЛЕВОЙ панели и запускает «Добавить книгу» (решение владельца 04.08); дублирующая строка «+ Добавить книгу» из дерева убрана — одна вещь, одно место. В центре и справа `+` нет и не будет: наборы вкладок там фиксированные, пользователь их не расширяет, и кнопка вышла бы декоративной — ровно то, что эта строка запрещает. Состояния наведения перестали быть «непроверенными»: они сняты не статичным кадром, а прогоном браузера (`data-hovered`/`data-selected`/`data-focus-visible` у кнопки, вкладки, строки дерева и списка). **Осталось на S6:** приглушённая колонка оригинала — решение, а не замер, пересмотреть на настоящей читалке | ~~S2~~ · S6 | S1, сверка с fleet.png |
|
||||
|
|
|
|||
|
|
@ -441,14 +441,36 @@ JSX-спред `<div {...props} />` со `style` внутри объекта ·
|
|||
в списке подобранных после удаления из `tokens.css` · число в `measures.ts`, разошедшееся
|
||||
с одноимённым токеном · модуль стилей, который никто не импортирует.
|
||||
|
||||
**Хвосты ревью оркестратора (04.08), 11 форм — один прогон линтера, 7 падений и 4 прохода.**
|
||||
Падают: динамический `import('../mock/book')` · динамический `import('../tokens/tokens.css')` ·
|
||||
динамический `import('react-aria-components')` вне `src/ui/` · то же из `src/api/` (фикстуры ему
|
||||
можно, примитивы — нет) · динамический импорт моков из `src/ui/` · тернарник в значении
|
||||
CSS-переменной (`{'--x': flag ? 'red' : 'blue'}`) · шаблонная строка там же.
|
||||
Проходят (и обязаны): динамический импорт `*.module.css` · идентификатор в значении переменной ·
|
||||
`import('react-aria-components')` внутри `src/ui/` · `import('../mock/book')` внутри `src/api/`.
|
||||
Причина всей пачки одна: `no-restricted-imports` разбирает только ОБЪЯВЛЕНИЕ импорта, а
|
||||
`await import(…)` — выражение, поэтому каждый шов продублирован селектором и точечные послабления
|
||||
у обеих половин совпадают по имени шва.
|
||||
|
||||
**Языковая ветка в CSS (`generality.test.ts`, вторая половина).** Падает `:lang(zh) .source { … }`
|
||||
в любом файле стилей — проверено живым нарушением с восстановлением файла из памяти, не из git.
|
||||
Комментарии из CSS снимаются перед проверкой: правило объясняется в том же файле, где действует,
|
||||
и объяснение содержит запрещённую форму дословно.
|
||||
|
||||
**Чего гейты по-прежнему не видят — перечислено, потому что «закрыто» без границ и есть та самая
|
||||
ловушка, о которой этот раздел** (найдено адверсариальным ревью S2, каждая граница воспроизведена):
|
||||
|
||||
- **вычисленное значение цвета.** Гейт на цветных атрибутах ловит литерал и шаблонную строку;
|
||||
`const c = 'red'; <svg fill={c}/>`, тернарник, вызов функции и JSX-спред проходят. Синтаксически
|
||||
это не ловится, а типовое правило под такую проверку в `typescript-eslint` отсутствует;
|
||||
- **значение CSS-переменной, собранное выражением** (`style={{'--x': compute()}}`) — проверять
|
||||
статически нечего;
|
||||
- **значение CSS-переменной, собранное вызовом или вынесенное в идентификатор**
|
||||
(`style={{'--x': compute()}}`, `style={{'--x': value}}`) — проверять статически нечего, а запрет
|
||||
идентификатора убил бы само исключение. Тернарник и шаблонная строка с 04.08 ловятся: это
|
||||
носители литерала, и обход через них был буквальным;
|
||||
- **значения стилей в тесте общности.** `generality.test.ts` видит разметку и селекторы, но не
|
||||
значения: именованная гарнитура письменности в токене (`--font-cjk: 'Noto Sans CJK SC', …`)
|
||||
действует как `:lang(zh)`, только молча. Ровно такой токен и приехал в S2 — снял его человек
|
||||
на ревью, не гейт; список именованных фейсов в правиле был бы хрупким по построению;
|
||||
- **императивный стиль** (`node.style.padding = '13px'`, `setAttribute('style', …)`) — в коде
|
||||
такого нет ни одного случая, но правилом это не запрещено;
|
||||
- **отключение TSX-гейта комментарием.** Оно обязано называть правило и нести причину, но проходит;
|
||||
|
|
|
|||
|
|
@ -33,20 +33,17 @@
|
|||
|
||||
Ты фронтенд-сессия. **Зона записи — только `frontend/`.** `backend/`, `platform/`, `docs/`, `eval/` — read-only; расхождения и вопросы — пингом владельцу, не правкой чужой зоны.
|
||||
|
||||
### ⚠ Коммит-права и git-протокол (решение владельца 02.08, D39.88 — читать ДО первого коммита)
|
||||
### ⚠ Зона и git (читать ДО первой строки кода)
|
||||
|
||||
**Право коммита у тебя ЕСТЬ — но ровно на свою зону: «свою часть коммить, чужую не трогать НИКОГДА».**
|
||||
**Ты не коммитишь — лендит оркестратор.** Твоё дело: довести дерево до зелёного `npm run check`,
|
||||
описать сделанное в `frontend/docs/PROGRESS.md` и передать.
|
||||
|
||||
1. **Коммить только пути внутри `frontend/`.** Ничего за её пределами — ни `docs/`, ни `backend/`, ни `platform/`, ни корневых файлов, ни `.gitignore`. Нужна правка вне зоны — пинг владельцу, её сделает оркестратор.
|
||||
2. **Форма коммита — ТОЛЬКО с явным списком путей:** `git commit -- frontend/<файл> frontend/<файл>`. Это не стилистика, а механика: **голый `git commit` уносит ВЕСЬ индекс**, включая файлы, застейдженные параллельной сессией, — и чужая работа уезжает под твоим сообщением. Так уже произошло ДВАЖДЫ 02.08 (`e9a6bb2`, `b98afb5` унесли доки оркестратора). Pathspec-форма чужой индекс не трогает.
|
||||
3. **НИКОГДА `git add -A`, `git add .`, `git commit -a`.** Только явные пути.
|
||||
4. **Перед каждым коммитом:** `git status` (опознать чужое) → `git diff --cached --name-only`. Увидел в индексе чужой путь — **не «прибирай» его**, просто коммить pathspec-формой, и он останется на месте.
|
||||
5. **Не оставляй свои файлы застейдженными** между шагами: пока они в индексе, их может унести чужой коммит. Стейдж и коммит — одной командой.
|
||||
6. **НИКАКИХ** `reset --hard`, `amend`/`rebase` несвежих коммитов, `checkout` поверх грязного дерева, перезаписи истории. Параллельные сессии — норма, их незакоммиченные файлы не трогать.
|
||||
7. **Чужое всё-таки уехало в твой коммит?** Историю НЕ переписывать — сообщить владельцу; содержимое цело, теряется только атрибуция.
|
||||
8. Никогда не коммитить: `START_PROMT.MD` (файл владельца), `.claude/settings.local.json`.
|
||||
1. **Писать только внутрь `frontend/`.** Ничего за её пределами — ни `docs/`, ни `backend/`, ни `platform/`, ни корневых файлов, ни `.gitignore`. Нужна правка вне зоны — пинг владельцу, её сделает оркестратор.
|
||||
2. **Чужие незакоммиченные файлы в дереве не трогать.** Параллельные сессии — норма: то, что лежит рядом изменённым, принадлежит не тебе.
|
||||
3. **НИКАКИХ** `git add -A`, `git add .`, `reset --hard`, `amend`/`rebase`, `checkout` поверх грязного дерева и любой перезаписи истории.
|
||||
4. Никогда не готовить к лендингу: `START_PROMT.MD` (файл владельца), `.claude/settings.local.json`.
|
||||
|
||||
Общие правила проекта — в `CLAUDE.md` в корне (там же эта норма в каноническом виде). Коммиты: английский, одно предложение ≤30 слов, без Co-Authored-By.
|
||||
Общие правила проекта — в `CLAUDE.md` в корне (там же эта норма в каноническом виде).
|
||||
|
||||
## Что за продукт
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,11 @@
|
|||
В-2 копирайт.
|
||||
- **Ждёт движка:** манифест глав и пофазный прогресс (единый бэклог, строки 99–100),
|
||||
к S5 — машиночитаемая таблица подписи банка (строка 101).
|
||||
- `npm run check` зелёный (5 шагов, 62 теста), `npm run check:full` зелёный, гейт доступности
|
||||
зелёный на обоих маршрутах. Гейтов стало больше: включён Ф-4 (отступы), закрыты Ф-9 и Ф-10.
|
||||
- `npm run check` зелёный (5 шагов, 77 тестов), `npm run check:full` зелёный, гейт доступности
|
||||
зелёный на обоих маршрутах. Гейтов стало больше: включён Ф-4 (отступы), закрыты Ф-9 и Ф-10,
|
||||
по ревью оркестратора 04.08 добавлены динамические импорты швов и языковая ветка в CSS.
|
||||
- **Карта канона для фронт-сессии — `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`.
|
||||
|
|
@ -86,8 +85,13 @@
|
|||
изобретать 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`;
|
||||
status · source · since_ch/until_ch`. Статусы подписи ровно три: `auto|draft|approved`,
|
||||
провенанс `seed|ruby|auto` плюс `mined` (ставится майнингом, `pipeline/mining.go:424`).
|
||||
**Типы — исправлено 04.08 по ревью оркестратора:** сама колонка `type` в схеме свободный текст,
|
||||
и её комментарий перечень не закрывает («name|term|title|…»), поэтому истина — гейты в Go:
|
||||
классификатор знает четыре (`terminology/classify.go:15`), банкнота принимает их же плюс
|
||||
`nickname` (`pipeline/banknote.go:74`). Итого пять: `name|place|title|term|nickname`.
|
||||
`org` и `fullname` движок не знает вовсе — они приехали в тип фронта из этой самой строки;
|
||||
- пары — `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`.
|
||||
|
|
@ -201,6 +205,43 @@ NDJSON — это шов ДВИЖОК↔ПЛАТФОРМА (D39.85), а фрон
|
|||
|
||||
## Хроника
|
||||
|
||||
### 04.08 — ревью оркестратора по S2 (`ece94ef`): что дофиксили
|
||||
|
||||
Ревью принято целиком; каждая находка проверена по коду движка ДО правки, а не по формулировке
|
||||
ревью. Три главных:
|
||||
|
||||
1. **Токен `--font-cjk` нарушал собственный пин** «CJK — системный стек + обязательный `lang`»
|
||||
(`STACK_DECISIONS.md` §2): в нём стояли именованные гарнитуры, и все три — упрощённый
|
||||
китайский (`Noto Sans CJK SC`, `PingFang SC`, `Microsoft YaHei`). Именованный фейс перекрывает
|
||||
языковой подбор браузера, то есть японские кандзи получили бы китайские начертания
|
||||
(унификация Хань) при живом и правильном `lang="ja"`. **Из двух предложенных лечений выбрано
|
||||
первое — снять фейсы, а не скоупить `:lang()`:** языковая ветка в CSS — это ровно та
|
||||
пар-специфика в коде, которую запрещает §2 канона, и новой паре понадобилась бы правка стилей.
|
||||
Токен удалён, у колонки оригинала гарнитуры больше нет: подбор делает браузер по `lang`
|
||||
из данных пары.
|
||||
2. **`org` выкинут из `BankTerm['kind']`.** Движок его не знает: закрытый словарь —
|
||||
`terminology/classify.go:15` (четыре типа) и `pipeline/banknote.go:74` (те же плюс `nickname`).
|
||||
Исправлена и строка журнала, из которой этот тип приехал: она приписывала перечень
|
||||
`migrate.go:176`, где перечня нет — там свободный текст и открытый комментарий.
|
||||
3. **Ключ ряда банка был `term.source`** — это ломается на полисемии: у движка термин уникален
|
||||
по (`src`, `sense`, `since_ch`, `until_ch`), и один иероглиф законно приходит несколькими
|
||||
строками. До приезда полей (Ф-14) ключ составной из того, что есть.
|
||||
|
||||
Мелочь оттуда же: ранг `content_filter` в карте вердиктов = 0, не 1 (`status.go:152` — он в одной
|
||||
группе с отказами); в текущем состоянии висели два счёта тестов сразу (31 и 62) — снят устаревший;
|
||||
у пары `r2-2` замечание «подписанный термин не применён» было фантомным — в тексте промаха не было,
|
||||
теперь он там настоящий («Фан Юэнь» против подписанного «Фан Юань»), иначе витрина показывала
|
||||
предупреждение о том, чего на экране нет.
|
||||
|
||||
**Хвосты в гейты, тоже из ревью** (каждая форма проверена живым нарушением — 7 падений и 4 прохода
|
||||
одним прогоном, `FRONTEND_PLAN.md` §5.4.1): `no-restricted-imports` видит только статический
|
||||
импорт, поэтому три шва (моки · глобальный CSS · библиотека примитивов) продублированы селекторами
|
||||
на `ImportExpression` — с теми же точечными послаблениями, что у статической половины; в гейте
|
||||
CSS-переменной закрыты носители литерала — тернарник и шаблонная строка. Плюс `generality.test.ts`
|
||||
получил вторую половину: языковая ветка `:lang(…)` в стилях теперь падает тестом, а не остаётся
|
||||
на глазах ревьюера, и в самом тесте названа граница — значения стилей он по-прежнему не видит
|
||||
(именно там и пряталась находка №1).
|
||||
|
||||
### 04.08 — третья фронт-сессия (S2): оболочка трёх панелей, слой `src/ui/`, масштаб и гейты
|
||||
|
||||
**Построено.**
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Промт: фронт-сессия S2 — оболочка трёх панелей и слой `src/ui/`
|
||||
|
||||
Ты — фронтенд-сессия TextMachine, третья по счёту. Зона записи — **только `frontend/`**;
|
||||
право коммита есть, ровно на свою зону, pathspec-формой (протокол — стоящий промт
|
||||
`FRONTEND_SESSION_PROMPT.md` §«Коммит-права», он в силе целиком). Пре-коммит хук уже стоит
|
||||
сессия не коммитит — лендит оркестратор (протокол — стоящий промт
|
||||
`FRONTEND_SESSION_PROMPT.md` §«Зона и git», он в силе целиком). Пре-коммит хук уже стоит
|
||||
и самоустанавливается при `npm install`: коммит с frontend-путями не пройдёт без зелёного
|
||||
`npm run check`, смесь зон и файлы «никогда не коммитить» блокируются. Хук — страховка,
|
||||
не замена дисциплины.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,27 @@ const primitives = {
|
|||
message: 'Примитивы живут в src/ui/. Оболочка и экраны ходят в наши обёртки, не в библиотеку.',
|
||||
};
|
||||
|
||||
// Те же три шва, но для ДИНАМИЧЕСКОГО импорта: `no-restricted-imports` разбирает только
|
||||
// объявление `import … from`, а `await import('../mock/book')` — выражение, и мимо шва проходило
|
||||
// всё три раза. Ключи совпадают с именами шаблонов выше, чтобы точечное послабление снималось
|
||||
// по имени шва, а не переписыванием селектора. `?inline`-суффикс не ловится ни здесь, ни в
|
||||
// статической половине — поведение у двух половин одинаковое, и это осознанно.
|
||||
const dynamicSeam = {
|
||||
globalCss: {
|
||||
selector: String.raw`ImportExpression > Literal[value=/\.css$/][value!=/\.module\.css$/]`,
|
||||
message: globalCss.message,
|
||||
},
|
||||
mockFixtures: {
|
||||
selector: String.raw`ImportExpression > Literal[value=/(^|\/)mock\//]`,
|
||||
message: mockFixtures.message,
|
||||
},
|
||||
primitives: {
|
||||
selector: "ImportExpression > Literal[value='react-aria-components']",
|
||||
message: primitives.message,
|
||||
},
|
||||
};
|
||||
const seams = (...names) => names.map((name) => dynamicSeam[name]);
|
||||
|
||||
const tokenGate = [
|
||||
...hexLengths.map((n) => ({
|
||||
selector: `Literal[value=/#[0-9a-fA-F]{${n}}\\b/]`,
|
||||
|
|
@ -86,6 +107,14 @@ const tokenGate = [
|
|||
selector: `JSXAttribute[name.name='style'] > JSXExpressionContainer > ObjectExpression > Property[key.value=/^--/] > Literal[value!=${String.raw`/^(--|var\(--|[0-9.]+$)/`}]`,
|
||||
message: inlineStyle,
|
||||
},
|
||||
// Носители литерала, которые проверка значения не видит насквозь: `cond ? 'red' : 'blue'`
|
||||
// и `` `${x}px` `` кладут в переменную ровно то, что запрещено строкой выше. Идентификатор
|
||||
// (`'--progress': value`) остаётся разрешённым — иначе исключение теряет смысл; вычисленное
|
||||
// значение проходит через именованную переменную, где оно осознанный шаг, а не инлайн.
|
||||
...['ConditionalExpression', 'TemplateLiteral'].map((node) => ({
|
||||
selector: `JSXAttribute[name.name='style'] > JSXExpressionContainer > ObjectExpression > Property[key.value=/^--/] > ${node}`,
|
||||
message: inlineStyle,
|
||||
})),
|
||||
// Остаточная дыра из §5.4: `<div {...props}/>`, где style приезжает внутри объекта. Запрещаем
|
||||
// само свойство `style` в объектных литералах — тогда носитель ловится там, где он собран.
|
||||
{ selector: "Property[key.name='style']", message: inlineStyle },
|
||||
|
|
@ -128,7 +157,11 @@ export default tseslint.config(
|
|||
globals: { ...globals.browser, ...globals.node },
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-syntax': ['error', ...tokenGate],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
...tokenGate,
|
||||
...seams('globalCss', 'mockFixtures', 'primitives'),
|
||||
],
|
||||
// Отключение обязано называть правило и объяснять причину. ⚠ Это НЕ полная симметрия
|
||||
// с CSS-половиной: там `reportDisables` делает ошибкой даже именованное отключение,
|
||||
// а здесь именованное с причиной проходит и гейт цвета на этой строке снимает. Такого
|
||||
|
|
@ -170,17 +203,26 @@ export default tseslint.config(
|
|||
{
|
||||
// Точка сборки глобальных стилей: сброс, токены и шрифты подключаются здесь и больше нигде.
|
||||
files: ['src/main.tsx'],
|
||||
rules: { 'no-restricted-imports': 'off' },
|
||||
rules: {
|
||||
'no-restricted-imports': 'off',
|
||||
'no-restricted-syntax': ['error', ...tokenGate, ...seams('mockFixtures', 'primitives')],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Контракт-тест обязан называть замеренные цвета в лицо — иначе ему нечего сверять.
|
||||
// Гейт цвета снимается, швы остаются: их этому тесту нарушать незачем.
|
||||
files: ['src/tokens/*.test.ts'],
|
||||
rules: { 'no-restricted-syntax': 'off' },
|
||||
rules: {
|
||||
'no-restricted-syntax': ['error', ...seams('globalCss', 'mockFixtures', 'primitives')],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Единственное место, где легальна библиотека примитивов. Остальные запреты в силе.
|
||||
files: ['src/ui/**'],
|
||||
rules: { 'no-restricted-imports': ['error', { patterns: [globalCss, mockFixtures] }] },
|
||||
rules: {
|
||||
'no-restricted-imports': ['error', { patterns: [globalCss, mockFixtures] }],
|
||||
'no-restricted-syntax': ['error', ...tokenGate, ...seams('globalCss', 'mockFixtures')],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Единственное место, где легальны и фикстуры, и сеть.
|
||||
|
|
@ -189,6 +231,7 @@ export default tseslint.config(
|
|||
'no-restricted-imports': 'off',
|
||||
'no-restricted-globals': 'off',
|
||||
'no-restricted-properties': 'off',
|
||||
'no-restricted-syntax': ['error', ...tokenGate, ...seams('globalCss', 'primitives')],
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,13 @@ export interface Chapter {
|
|||
export interface BankTerm {
|
||||
source: string;
|
||||
target: string;
|
||||
kind: 'name' | 'nickname' | 'title' | 'place' | 'org' | 'term';
|
||||
/**
|
||||
* Закрытый словарь типов движка: классификатор знает четыре (`terminology/classify.go:15`),
|
||||
* банкнота принимает их же плюс `nickname` (`pipeline/banknote.go:74`). Колонка `type` в схеме —
|
||||
* свободный текст с открытым перечнем в комментарии (`store/migrate.go:176`), поэтому истина
|
||||
* здесь — гейты в Go, а не DDL; `org` и `fullname` движок не знает вовсе.
|
||||
*/
|
||||
kind: 'name' | 'nickname' | 'title' | 'place' | 'term';
|
||||
signed: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
import { expect, it } from 'vitest';
|
||||
|
||||
// Ревью-вопрос канона по умолчанию: «заработает ли пара языков, которой в репо ещё НЕТ,
|
||||
// без правки кода?» (CLAUDE.md §2). На фронте эта норма ломается ровно одним способом —
|
||||
// код письменности, зашитый в разметку: `lang="zh"` переживёт приезд ja→ru молча, и японские
|
||||
// кандзи отрисуются китайскими начертаниями, а скринридер возьмёт китайский голос.
|
||||
// Найдено ревью S1 в живом коде витрины, поэтому проверяется машиной, а не глазами.
|
||||
// без правки кода?» (CLAUDE.md §2). На фронте эта норма ломается двумя способами: код
|
||||
// письменности, зашитый в разметку (`lang="zh"` переживёт приезд ja→ru молча, и японские кандзи
|
||||
// отрисуются китайскими начертаниями, а скринридер возьмёт китайский голос), и языковая ветка
|
||||
// в стилях (`:lang(zh) { … }` — та же пар-специфика, только записанная в CSS: новой паре
|
||||
// понадобилась бы правка стилей). Первое найдено ревью S1 в живом коде витрины, второе — ревью
|
||||
// оркестратора S2, поэтому проверяется машиной, а не глазами.
|
||||
//
|
||||
// ⚠ ГРАНИЦА, названная явно: тест видит разметку и СЕЛЕКТОРЫ, но не ЗНАЧЕНИЯ стилей. Пар-специфика
|
||||
// умеет прятаться и в значении — именованная гарнитура письменности («Noto Sans CJK SC» в токене
|
||||
// шрифта) действует как `:lang(zh)`, только молча и без селектора. Ровно такой токен и приехал
|
||||
// в S2; снял его человек на ревью, не гейт. Перечень таких границ — `FRONTEND_PLAN.md` §5.4.1.
|
||||
// Опции — литерал на каждом вызове: `import.meta.glob` разбирается сборщиком статически
|
||||
// и вынесенную константу не принимает.
|
||||
const components = import.meta.glob('./**/*.tsx', {
|
||||
query: '?raw',
|
||||
eager: true,
|
||||
import: 'default',
|
||||
});
|
||||
const styleSheets = import.meta.glob('./**/*.css', {
|
||||
query: '?raw',
|
||||
eager: true,
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
it.each(Object.keys(components))('%s не зашивает язык в разметку', (file) => {
|
||||
const literals = [...(components[file] as string).matchAll(/\blang=(["'][^"']*["'])/g)].map(
|
||||
|
|
@ -17,3 +31,11 @@ it.each(Object.keys(components))('%s не зашивает язык в разм
|
|||
);
|
||||
expect(literals, 'lang берётся из данных книги: lang={book.sourceLanguage}').toEqual([]);
|
||||
});
|
||||
|
||||
it.each(Object.keys(styleSheets))('%s не ветвится по языку', (file) => {
|
||||
// Комментарии снимаются: правило обязано объясняться в том же файле, где действует, а
|
||||
// объяснение содержит запрещённую форму дословно — иначе тест ловит собственную документацию.
|
||||
const css = (styleSheets[file] as string).replaceAll(/\/\*[\S\s]*?\*\//g, '');
|
||||
const selectors = [...css.matchAll(/:lang\([^)]*\)/g)].map((m) => m[0]);
|
||||
expect(selectors, 'языковая специфика живёт в данных пары, а не в селекторе').toEqual([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export const bankTerms: BankTerm[] = [
|
|||
// Соответствие, с которого сняты фразы ниже, — чтобы автору контракта было от чего оттолкнуться:
|
||||
//
|
||||
// движок | важность (status.go:150-175) | продуктовая фраза
|
||||
// content_filter = 1 | attention | Блок не переведён: содержимое отклонено
|
||||
// content_filter = 0 | attention | Блок не переведён: содержимое отклонено
|
||||
// sanitizer_defect = 2 | attention | Блок не переведён: ответ пришёл повреждённым
|
||||
// loop_degenerate = 3 | attention | Перевод зациклился, блок нужно повторить
|
||||
// glossary_miss = 5 | attention | Подписанный термин не применён в переводе
|
||||
|
|
@ -116,10 +116,13 @@ export const pairs: Pair[] = [
|
|||
note: { severity: 'glance', text: 'Служебная разметка вычищена автоматически' },
|
||||
},
|
||||
{
|
||||
// ⚠ «Фан Юэнь» — не опечатка фикстуры, а сам промах: 方源 подписан в банке выше как
|
||||
// «Фан Юань», а перевод пришёл с другой транслитерацией. Замечание обязано быть видно
|
||||
// в тексте, иначе витрина показывает предупреждение о том, чего на экране нет.
|
||||
id: 'r2-2',
|
||||
source: '但是现在,方源信了,彻底地信了。铁一般的事实摆在他的面前,不容他反驳——他真的重生了!',
|
||||
target:
|
||||
'Но теперь Фан Юань уверовал полностью и бесповоротно. Перед ним лежал железный факт, не допускающий возражений: он действительно переродился!',
|
||||
'Но теперь Фан Юэнь уверовал полностью и бесповоротно. Перед ним лежал железный факт, не допускающий возражений: он действительно переродился!',
|
||||
note: { severity: 'attention', text: 'Подписанный термин не применён в переводе' },
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const syllables = [
|
|||
['酒', 'Цзю'],
|
||||
] as const;
|
||||
|
||||
const kinds: BankTerm['kind'][] = ['name', 'nickname', 'title', 'place', 'org', 'term'];
|
||||
const kinds: BankTerm['kind'][] = ['name', 'nickname', 'title', 'place', 'term'];
|
||||
|
||||
export const scaleTerms: BankTerm[] = Array.from({ length: 1200 }, (_, index) => {
|
||||
const first = syllables[index % syllables.length] ?? syllables[0];
|
||||
|
|
|
|||
|
|
@ -93,7 +93,13 @@ const matches = (query: string) => (term: BankTerm) => {
|
|||
|
||||
function termRow(term: BankTerm, sourceLanguage: string): ListRow {
|
||||
return {
|
||||
id: term.source,
|
||||
// Ключом ряда не может быть один `source`: у движка термин уникален по
|
||||
// (`src`, `sense`, `since_ch`, `until_ch`) — `sense` там прямо назван разрешителем полисемии
|
||||
// (`store/migrate.go:176-190`), то есть один и тот же иероглиф законно приходит несколькими
|
||||
// строками с разными смыслами и окнами глав. Совпадение ключей у списка не «сливает
|
||||
// дубликат», а роняет ряд: React выбрасывает второй, RAC теряет выделение. Пока полей
|
||||
// различения нет в контракте (BACKLOG Ф-14), ключ собран из того, что есть.
|
||||
id: [term.source, term.target, term.kind].join('|'),
|
||||
text: `${term.source} ${term.target}`,
|
||||
content: (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@
|
|||
}
|
||||
|
||||
/* Приглушённая колонка оригинала — решение, а не замер: во Fleet на этом месте подсвеченный код.
|
||||
Пересмотреть на настоящей читалке (BACKLOG Ф-7). */
|
||||
Пересмотреть на настоящей читалке (BACKLOG Ф-7).
|
||||
Гарнитуру оригиналу НЕ назначаем: подбор по языку делает браузер по `lang` из данных пары
|
||||
(`STACK_DECISIONS.md` §2). Любой именованный фейс здесь пар-специфичен — стек SC отрисовал бы
|
||||
японские кандзи китайскими начертаниями (унификация Хань), а корейскому и латинице не подошёл
|
||||
бы вовсе; `:lang()`-ветка была бы той же пар-спецификой, только записанной в CSS. */
|
||||
.source {
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-cjk);
|
||||
}
|
||||
|
||||
.target {
|
||||
|
|
|
|||
|
|
@ -61,10 +61,6 @@
|
|||
/* --- Типографика; кегли подобраны по высоте глифов, померить их больше негде --- */
|
||||
--font-ui: 'Inter Variable', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono Variable', ui-monospace, monospace;
|
||||
|
||||
/* CJK отдаём системному стеку; lang на элементе приходит из данных пары */
|
||||
--font-cjk:
|
||||
'Noto Sans CJK SC', 'Source Han Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
--font-size-ui: 13px;
|
||||
--font-size-small: 12px;
|
||||
--font-size-content: 13px;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ const chosen = [
|
|||
'--space-6',
|
||||
'--font-ui',
|
||||
'--font-mono',
|
||||
'--font-cjk',
|
||||
'--font-size-ui',
|
||||
'--font-size-small',
|
||||
'--font-size-content',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue