diff --git a/frontend/README.md b/frontend/README.md index c3778f2b..2425101f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,6 +33,8 @@ npm run shot # снимок витрины в .shots/ — открыть - [`docs/FRONTEND_PLAN.md`](docs/FRONTEND_PLAN.md) — план S0: сверенные пины, карта `src/`, слои каскада, правила поддерживаемости в проверяемой форме, протокол скриншот-цикла, перепроверенные замеры референса и список расхождений витрины. +- [`docs/PROGRESS.md`](docs/PROGRESS.md) — журнал зоны: текущее состояние, решения владельца + по продукту, открытые вопросы, хроника сессий. - [`docs/BACKLOG.md`](docs/BACKLOG.md) — зонный бэклог фронта. - `references/` — три скриншота-референса (Fleet, Antigravity). diff --git a/frontend/docs/BACKLOG.md b/frontend/docs/BACKLOG.md index 872cce1d..ccf8c9d8 100644 --- a/frontend/docs/BACKLOG.md +++ b/frontend/docs/BACKLOG.md @@ -8,7 +8,7 @@ | Ф-2 | **React Compiler** — включить ОТДЕЛЬНЫМ шагом после заморозки интерфейса, с CI-проверкой, что вставки реально попали в бандл (в MVP выключен; совместимость держит линт eslint-plugin-react-hooks) | после заморозки UI | STACK_DECISIONS §1 | | Ф-3 | **Пересмотр TS 6 → TS 7** после выхода TS 7.1 с программным API (~октябрь 2026); решение обратимо — тайпчек не участвует в сборке | триггер: релиз TS 7.1 | STACK_DECISIONS §1 | | Ф-4 | **Токен-гейт на отступы** (`padding`/`margin`/`gap`/`border-radius`) — вторым шагом, когда шкала токенов зафиксирована (иначе гейт мешает подбору) | **триггер наступил**: витрина S1 снята, шкала `--space-1…6` заведена; включать, когда S2 обкатает её на настоящей оболочке | STACK_DECISIONS §3 | -| Ф-8 | **Вопрос владельцу: закрывать ли `/* eslint-disable */` в TSX.** CSS-половина гейта цвета обхода комментарием больше не допускает (`reportUnscopedDisables`), TSX-половина — допускает. Закрывается одной строкой `linterOptions: { noInlineConfig: true }`, но той же строкой запрещаются легитимные точечные подавления (`react-hooks/exhaustive-deps`), которые рекомендует сам React. Выбор не технический, а владельческий | решение до S2 | адверсариальное ревью S1 | +| ~~Ф-8~~ | ~~Вопрос владельцу: закрывать ли `/* eslint-disable */` в TSX~~ — **закрыт 02.08 без решения владельца.** Вопрос был поставлен как выбор из двух плохих вариантов, а у него есть отраслевой третий: `@eslint-community/eslint-plugin-eslint-comments` 4.7.2 (`no-unlimited-disable` + `require-description` + `disable-enable-pair`). Голое отключение падает тремя ошибками, точечное подавление с причиной проходит. Урок: не выносить владельцу технический вопрос, не поискав отраслевую практику | закрыт | адверсариальное ревью S1 | | Ф-9 | **Хвосты гейтов после адверсариального ревью S1** (все воспроизведены исполнением, ни один не в боевом коде): именованные цвета CSS в TSX (`fill="red"`, ``) не ловятся — в CSS запрещены, асимметрия; санкционированное исключение `style={{'--x': …}}` пропускает любое значение, включая литеральный цвет · JSX-спред `
` со `style` внутри · системные цветовые ключевые слова (`Highlight`, `ButtonText`) проходят в `box-shadow`/`outline`/`caret-color` и в сокращённых записях | до S2 или в ней | адверсариальное ревью S1 | | Ф-10 | **Хвосты структурных тестов**: `cssModules.test.ts` строит пары по имени файла (компонент, импортирующий соседний модуль, из проверки выпадает), не видит деструктуризацию `const { shell } = styles`, а `classesOf` тянет в «объявленные классы» расширения из `url(...)`. `tokens.test.ts` сверяет 23 токена из 38 и не имеет замка полноты — новый токен приезжает без гейта по построению | до S2 | адверсариальное ревью S1 | | Ф-7 | **Хвосты сведения витрины с референсом** (`FRONTEND_PLAN.md` §5.2): `+` в конце рядов вкладок — вместе с действием, которое он запускает (S2) · трактовка колонки оригинала приглушённым цветом — решение, а не замер, пересмотреть на настоящей читалке (S6) · состояния наведения витриной не проверены, снимок статичен | S2 · S6 | S1, сверка с fleet.png | diff --git a/frontend/docs/FRONTEND_PLAN.md b/frontend/docs/FRONTEND_PLAN.md index 03b521fe..26ddcc5b 100644 --- a/frontend/docs/FRONTEND_PLAN.md +++ b/frontend/docs/FRONTEND_PLAN.md @@ -126,7 +126,7 @@ CJK-шрифт не ставим: иероглифы отдаём системн | # | Правило | Проверка | |---|---|---| | 1 | Цвет и размер только из `tokens.css` | **машинно**: stylelint `declaration-property-value-allowed-list` (`/^var\(--/` на `color`, `background-color`, `border-color`, `fill`, `stroke`, `font-size`, `z-index`) + ESLint `no-restricted-syntax` на `#hex`/`rgb(`/`hsl(`/`oklch(` в TSX. Исключение по пути — только `tokens/` | -| 2 | Отключить CSS-гейт комментарием нельзя | **машинно**: `reportDisables: true` на правилах + `reportUnscopedDisables: true` в корне конфига. Одного `reportDisables` НЕ хватает: он ловит только именованное отключение, а голое `/* stylelint-disable */` в шапке снимало гейт молча — проверено. **В TSX дыра открыта:** `/* eslint-disable */` работает, `noInlineConfig` сознательно не включён (он же запретит легитимные точечные подавления `react-hooks/exhaustive-deps`). Решение владельца — `BACKLOG.md` Ф-8 | +| 2 | Отключить CSS-гейт комментарием нельзя | **машинно**: `reportDisables: true` на правилах + `reportUnscopedDisables: true` в корне конфига. Одного `reportDisables` НЕ хватает: он ловит только именованное отключение, а голое `/* stylelint-disable */` в шапке снимало гейт молча — проверено. В TSX то же самое делают правила `@eslint-community/eslint-comments`: `no-unlimited-disable` требует назвать правило, `require-description` — написать причину, `disable-enable-pair` — закрыть область. Голое `/* eslint-disable */` даёт три ошибки; точечное `// eslint-disable-next-line react-hooks/exhaustive-deps -- причина` проходит. Бинарный `noInlineConfig` не берём: он запрещает и то, что рекомендует сам React | | 3 | Данные только через `src/api/` | ревью-вопрос: есть ли `fetch`/`axios`/импорт из `mock/` вне `src/api/`? Должно быть «нет» | | 4 | Файл = один компонент + свой `.module.css`, больше ~150 строк — делить | ревью глазами при лендинге пакета | | 5 | Состояние ровно в двух местах: TanStack Query (серверное), Zustand (интерфейсное) | ревью-вопрос: есть ли `useState` с копией серверных данных? Должно быть «нет» | diff --git a/frontend/docs/PROGRESS.md b/frontend/docs/PROGRESS.md new file mode 100644 index 00000000..eef4579a --- /dev/null +++ b/frontend/docs/PROGRESS.md @@ -0,0 +1,112 @@ +# Журнал зоны «Фронт» + +> Зонный журнал: сессии фронта пишут сюда сами, работают независимо, в `docs/PROGRESS.md` +> оркестратора не пишут (решение владельца 02.08). Хвосты — `BACKLOG.md`, план и замеры — +> `FRONTEND_PLAN.md`, ратифицированные пины — `STACK_DECISIONS.md`. +> Сверху — текущее состояние, ниже — хроника по сессиям. + +## Текущее состояние + +- **Пройдено:** S0 (план) и S1 (инструменты, скриншот-цикл, `tokens.css`, витрина). +- **Дальше:** S2 — оболочка трёх панелей и слой `src/ui/`. +- **Ждёт владельца:** решений нет, все вопросы закрыты (см. хронику 02.08). +- **Ждёт движка:** ничего до S4. Затем — манифест глав и пофазный прогресс (строки 99–100 + единого бэклога), к S5 — машиночитаемая таблица подписи банка (строка 101). +- `npm run check` зелёный (5 шагов, 27 тестов), `npm run check:full` зелёный. + +## Решения владельца по продукту + +Записываю здесь, потому что они меняют раскладку из §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 реестра требований. + +## Открытые вопросы к владельцу + +| # | Вопрос | Почему это его вопрос | +|---|---|---| +| В-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 (первая фронт-сессия) + +**Заленжено:** `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-формой с явным списком путей. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 4a965f7b..37865888 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,3 +1,4 @@ +import comments from '@eslint-community/eslint-plugin-eslint-comments/configs'; import js from '@eslint/js'; import prettier from 'eslint-config-prettier'; import reactHooks from 'eslint-plugin-react-hooks'; @@ -61,12 +62,19 @@ export default tseslint.config( // .flat — именно flat-вариант: одноимённый ключ верхнего уровня остался в старом формате. reactHooks.configs.flat['recommended-latest'], reactRefresh.configs.vite, + comments.recommended, { languageOptions: { globals: { ...globals.browser, ...globals.node }, }, rules: { 'no-restricted-syntax': ['error', ...tokenGate], + // Симметрия с CSS-половиной, где голое отключение запрещено (reportUnscopedDisables). + // noInlineConfig не берём: он запрещает и точечные подавления, которые рекомендует + // сам React (react-hooks/exhaustive-deps). Отраслевая практика — не бинарный запрет, + // а требование называть правило и писать причину. + '@eslint-community/eslint-comments/no-unlimited-disable': 'error', + '@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }], // Глобальных стилей ровно два файла; всё остальное — CSS Modules, они скоупятся сами. 'no-restricted-imports': [ 'error', diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3f505799..7699f0ad 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "react-router": "8.3.0" }, "devDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", "@eslint/js": "10.0.1", "@testing-library/react": "16.3.2", "@types/node": "22.20.1", @@ -549,6 +550,36 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.7.2.tgz", + "integrity": "sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "ignore": "^7.0.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index c98adcd1..67259bd3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "react-router": "8.3.0" }, "devDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", "@eslint/js": "10.0.1", "@testing-library/react": "16.3.2", "@types/node": "22.20.1", diff --git a/frontend/src/showcase/Showcase.module.css b/frontend/src/showcase/Showcase.module.css index dda08117..fb69f464 100644 --- a/frontend/src/showcase/Showcase.module.css +++ b/frontend/src/showcase/Showcase.module.css @@ -38,13 +38,6 @@ min-height: 0; } -.center { - display: grid; - grid-template-rows: 1.7fr 1fr; - gap: var(--gap); - min-height: 0; -} - /* Панель — скруглённая карточка без рамок и теней: отделяют только промежуток и заливка. */ .panel { display: grid; @@ -77,6 +70,11 @@ color: var(--color-text-secondary); } +/* Подвал с двумя вещами по краям: действие слева, счётчик справа. */ +.panelFooterSplit { + justify-content: space-between; +} + .tab { display: flex; align-items: center; @@ -98,9 +96,9 @@ } .filter { - width: 220px; - height: var(--panel-header-height); - margin-left: auto; + width: 100%; + height: var(--row-height); + margin-bottom: var(--space-2); border: 1px solid transparent; padding-inline: var(--space-4); border-radius: var(--radius-control); @@ -203,8 +201,8 @@ .bankRow { display: grid; - grid-template-columns: 104px 1fr 88px 88px; - gap: var(--space-3); + grid-template-columns: auto 1fr; + gap: var(--space-4); align-items: center; height: var(--row-height); border-block: var(--row-fill-inset) solid transparent; @@ -213,20 +211,6 @@ background-clip: padding-box; } -.bankHead { - color: var(--color-text-muted); - font-size: var(--font-size-small); -} - -.fact { - display: grid; - grid-template-columns: 92px 1fr; - gap: var(--space-3); - align-items: center; - height: var(--row-height); - padding-inline: var(--space-2); -} - .dim { overflow: hidden; color: var(--color-text-secondary); diff --git a/frontend/src/showcase/Showcase.tsx b/frontend/src/showcase/Showcase.tsx index ab15f43a..ed768d16 100644 --- a/frontend/src/showcase/Showcase.tsx +++ b/frontend/src/showcase/Showcase.tsx @@ -3,7 +3,6 @@ import { ChevronDown, Download, FileText, - PanelBottom, PanelLeft, PanelRight, Plus, @@ -12,30 +11,29 @@ import { } from 'lucide-react'; import styles from './Showcase.module.css'; -import { bankRows, bookFacts, chapters, chapterStates, pairs } from './showcaseData'; +import { bankRows, chapters, chapterStates, flagReasonLabels, pairs } from './showcaseData'; // Тонкие линейные монохромные иконки как во Fleet (STACK_DECISIONS §2). const icon = { size: 16, strokeWidth: 1.5 } as const; /** - * Витрина — не экран продукта, а сверочная страница скриншот-цикла: панели, вкладки, - * строка дерева, строка таблицы и пара оригинал/перевод на реальном тексте. - * Кладётся рядом с references/fleet.png и сводится глазами (docs/FRONTEND_PLAN.md §4). + * Витрина — не экран продукта, а сверочная страница скриншот-цикла. Раскладка при этом + * несёт решения владельца 02.08 по структуре (docs/PROGRESS.md, запись «Раскладка»): + * навигация слева, документ в центре во всю высоту, справочное о читаемой главе справа. */ export function Showcase() { return (
+ {/* Кнопка есть ровно у той панели, которая существует: нижней панели в раскладке нет */}
-
-
Путь книгохранилища
+
Гу Чжэньжэнь
-
@@ -43,19 +41,18 @@ export function Showcase() {
Книги - Банк памяти Поиск
  • - Путь книгохранилища + Гу Чжэньжэнь
  • {chapters.map((chapter, index) => (
  • {chapter.title} @@ -72,86 +69,73 @@ export function Showcase() { Добавить главу
+ {/* Единственный вход в настройки во всём интерфейсе (промт §3.12) */}
Настройки
-
-
-
- - Глава 1. Перед рассветом - - Глава 2. Медный ключ -
-
- {pairs.map((pair) => ( -
-
- {pair.source} -
-
- {pair.target} -
- {pair.note &&

{pair.note}

} -
- ))} -
-
- -
-
- Банк памяти - {/* Единственное цветное пятно панели — акцентная рамка фокуса, как во Fleet. - autoFocus здесь ради снимка: иначе акцент на витрине не проверить. */} - -
-
-
- Термин - Перевод - Тип - Состояние -
- {bankRows.map((row, index) => ( -
- {row.term} - {row.translation} - {row.kind} - {row.state} +
+
+ + Раздел 2. Прозрение пятисот лет + + + Раздел 3. Церемония открытия + +
+
+ {pairs.map((pair) => ( +
+
+ {pair.source}
- ))} -
-
-
+
+ {pair.target || перевод не получен} +
+ {/* Показываем продуктовую фразу, а не вердикт движка: detail остаётся внутри */} + {pair.flagReason && ( +

{flagReasonLabels[pair.flagReason]}

+ )} + + ))} +
+
- О книге + О книге Замечания + Банк
-
- {bookFacts.map((fact) => ( -
-
{fact.label}
-
{fact.value}
+
+ + {bankRows.map((row) => ( +
+ {row.src} + {/* Неподписанный термин приглушён: состояние читается насыщенностью, не цветом */} + + {row.dst} +
))} -
+
+ {/* Связка справочного вида с рабочим: таблица подписи открывается вкладкой в центре, + где есть ширина под колонки, клавиатуру и массовые действия (промт §3.5) */} +
+ Подписать банк + подписано 9 из 11 +
- Путь книгохранилища / Глава 1. Перед рассветом - переведено 12 из 18 · zh → ru + Гу Чжэньжэнь / Раздел 2. Прозрение пятисот лет + переведено 118 из 2284 · zh → ru
); diff --git a/frontend/src/showcase/showcaseData.ts b/frontend/src/showcase/showcaseData.ts index 84bc09ee..6a807748 100644 --- a/frontend/src/showcase/showcaseData.ts +++ b/frontend/src/showcase/showcaseData.ts @@ -1,6 +1,14 @@ -// Материал витрины. Текст реальный — фрагмент backend/example/chapter1-zh.txt с русским -// переводом: выдуманный «Lorem ipsum» дал бы неверную плотность колонок, потому что кириллица -// длиннее латиницы, а иероглифы короче всего. +// Материал витрины. Формы данных сняты с движка, а не выдуманы — иначе моки разойдутся +// с настоящим контрактом ещё до того, как появится HTTP-слой. +// +// ⚠ Источник фактуры лежит ВНЕ репозитория, на диске стенда (`/home/ubuntu/books/gu-zhenren/`): +// прогоны, экспорты пар, банки. В git художественного текста почти нет, а оба +// `backend/example/sample-*.db` пустые — там только схема. Привязка данных к конкретной +// машине — открытый вопрос владельца, см. docs/PROGRESS.md. +// +// Фрагмент ниже — короткая выдержка из настоящего прогона `rerun2/export-dspro.json` +// (蛊真人, zh→ru). Взят объём, которого хватает на проверку плотности колонок, и не больше: +// исходник защищён авторским правом, и в репозиторий его целиком не кладут. // // Это ТОЛЬКО данные витрины. Продуктовые фикстуры живут в src/mock/ и заводятся на S3. @@ -15,57 +23,112 @@ export const chapterStates: Record = { + glossary_miss: 'Термин из этого места не подписан в банке памяти', + sanitizer_stripped: 'Часть блока не переведена, стоит перечитать', + excision_suspect: 'Возможен пропуск: перевод заметно короче оригинала', + loop_degenerate: 'Перевод зациклился, блок стоит перевести заново', + content_filter: 'Блок не переведён: содержимое отклонено', +}; + +// Единица пары — edit-unit из `tmctl export --pairs` (pipeline/export.go:25). Замечание +// адресует блок целиком: байтовых смещений у проверок движка нет, только счётчики, +// поэтому `flagReason` и `detail` — ровно то, что отдаёт движок. +export const pairs: { + source: string; + target: string; + flagReason?: string; + detail?: string; +}[] = [ + { + source: '第二节:逆光阴五百年觉悟', + target: 'Раздел 2: Прозрение пятисот лет против течения времени', }, { source: - '「师兄,我们真的可以进来吗?」美樱压低声音问道。她的手里,紧握着一把古旧的铜钥匙。\n' + - '「放心,长老已经准许了,」拓海答道,「趁天亮之前,把那卷古籍找出来。」', + '传说中,这个世界存在着一条光阴之河,支撑着这个世界的流转。而利用春秋蝉的力量,就能逆流而上,回到从前。\n' + + '对这个传闻,世人众说纷纭。很多人并不相信,有些人则将信将疑。\n' + + '几乎没有人真的确信。', target: - '— Брат, нам правда можно сюда? — понизив голос, спросила Мэйин. В руке она сжимала старый медный ключ.\n' + - '— Не тревожься, старейшина уже дал разрешение, — ответил Тохай. — Успеем до света найти тот свиток.', - note: 'Обращение к старшему по школе переведено двумя способами в пределах главы', + 'В преданиях говорилось, что в этом мире существует река времени, поддерживающая его течение, и с помощью силы Весенне-осенней цикады можно подняться против потока и вернуться в прошлое.\n' + + 'Об этом слухе люди судили по-разному. Многие не верили вовсе, иные верили наполовину.\n' + + 'Почти никто не был уверен по-настоящему.', + }, + { + source: '但是现在,方源信了,彻底地信了。铁一般的事实摆在他的面前,不容他反驳——他真的重生了!', + target: + 'Но теперь Фан Юань уверовал полностью и бесповоротно. Перед ним лежал железный факт, не допускающий возражений: он действительно переродился!', + flagReason: 'glossary_miss', + detail: 'В блоке встречается термин без подписи в банке памяти', }, { source: - '两人在书架之间静静穿行。窗外,东方的天际泛起一丝鱼肚白。尘埃的气味,与旧纸的清香,在空气中浮动。美樱停下脚步,向一本书伸出手去。', - target: - 'Они бесшумно шли между стеллажами. За окном восточный край неба уже отдавал бледной белизной. В воздухе плыл запах пыли и сухой аромат старой бумаги. Мэйин остановилась и потянулась к одной из книг.', + '因为每一次使用春秋蝉,都必须付出生命,将整个身躯和所有的修为统统献祭,作为驱动的力量。\n' + + '这个代价实在太昂贵了,更让人无法接受的是——往往付出了生命,也不知道结果怎样。', + target: '', + flagReason: 'content_filter', + detail: 'upstream refusal: content policy', }, ]; export const bookFacts: { label: string; value: string }[] = [ - { label: 'Название', value: 'Путь книгохранилища' }, + { label: 'Название', value: 'Гу Чжэньжэнь' }, { label: 'Оригинал', value: 'китайский' }, { label: 'Перевод', value: 'русский' }, { label: 'Жанр', value: 'сянься' }, - { label: 'Глав', value: '18' }, - { label: 'Объём', value: '412 тыс. знаков' }, + { label: 'Разделов', value: '2284' }, + { label: 'Объём', value: '7,78 млн знаков' }, { label: 'Добавлена', value: '28.07.2026' }, { label: 'Состояние', value: 'переводится' }, ];