Accept polygon package seven phase A as D39.43: miner recall measured, emission-layer attribution, terminologist cost amendment, land report and pkg7 tools

This commit is contained in:
Claude (backend session) 2026-07-26 10:08:38 +03:00
parent 996badefcb
commit 9f2a143a8d
11 changed files with 1982 additions and 2 deletions

File diff suppressed because one or more lines are too long

View file

@ -1090,3 +1090,13 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу
**Уточнения к записанному:** D39.36 п.3 «в инъекцию идёт только CONFIRMED» — верно ТОЛЬКО для редакторского блока; блок переводчика рендерит не-Confirmed с маркером (S1, тест). Моё объяснение владельцу «пометка нужна главам 450 того же прогона» было неверным для волновой архитектуры — взято назад (в волне черновики готовы до майнинга; пометка живёт на edit-волне авто-режима и в черновиках следующих частей). research/20 §C2 «не-мажоритарность» и §B3 «evidence-файл» — код расходится осознанно (S10/S11): §C2-формула уезжает в терминолога, артефакт кандидатов остаётся в сторе (спека амендирована этим блоком). Р7 «auto→approved автоматом» — superseded семантикой владельца D39.36/39.42.
**НЕ строить в фазе 2:** ось lane / durable-реестр P1 · ретирование `Gates.Banknote.Enabled` · направленная банкнота (вернуться по замеру покрытия терминолога) · неподвижная точка P5 · веб-фетч · M2-кэш (доктрина Д2 остаётся кандидатом на отдельную ратификацию).
## D39.43 — Пакет-7 фаза A ПРИНЯТА: recall боевого WHICH-канала впервые измерен (0.0690.103; term 0.040; ограничивает слой ЭМИССИИ, снимается до 0.911) — количественное основание терминолога; посылка «культивация = общепринятое» уликами НЕ подтверждена (вскрыта ложная сходимость: один английский глоссарий + один блогер); смета терминолога на полном банке = ЕДИНИЦЫ ДОЛЛАРОВ, не «центы» — амендмент D39.42 п.1 (26.07, оркестратор №8). ✅
**Приёмка исполнением (5 верификаторов, 26 проверок): 24 CONFIRMED / 1 PARTIAL / 1 UNVERIFIABLE / 0 REFUTED** — майнер-прогоны и проба рубрики воспроизведены побайтно на пине 996bade, веб-улики подтверждены дословно, арифметика пересчитана. Отчёт: `docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md` (ревью-шапка с нитами); инструменты: `eval/pkg7/`; $0.
**Три следствия для контракта:** (1) **Амендмент D39.42 п.1:** «батчи, дешёвая модель, центы/книга» для терминолога верно ТОЛЬКО на выборках; на полном банке (13 246 кандидатов расширенной эмиссии × ~2.5k токенов контекстов ≈ 33 млн вх. токенов) — единицы долларов; смета фазы 2 пака-20 обязана считаться от реального объёма входа, и требование G7 (отделить «что показать владельцу» от «что отдать терминологу») получает денежное обоснование. (2) **Фактура в пак-20 и будущий пак generic-майнера:** требования G1G10 (алфавит кандидатов = данные пары · многословные кандидаты · нормализация не выводит строку за пределы книги · порог частоты на класс · разметка исходника = сигнал · контраст на пару · ось эмиссии G7 · кластер не глотает родовой титул G8 · квадратичность ⇒ 4.6 ч на приёмочной книге G9 · тихая пустая дельта G10); точечный дефект кластеризации с адресом (`族长`/`学堂家老` проглочены кластером `葛家` — стоил подписанного терма). (3) **Правило жанровости (предложение сессии, разумное):** жанровое = встречается в ≥2 книгах жанра без смены смысла; книжное живёт с книгой.
**Ресёрч:** 84 самопроверки источников (62C/14P/7MISQ/1OA/0 фабрикаций) + критик полноты, снявший собственные «сходимости» сессии (полтора слоя вместо трёх у «12 полей»; круговая валидация K3). Посылка D39.42 п.1 об индустрии НЕ подтверждена и НЕ опровергнута: издательская сторона стоит на маркетинговых аннотациях (систематически смещены против жаргона), фан-сторона — ложная сходимость из одного первоисточника; предложен V0 (не подписывать 修炼-строку до $0-добора: тела ISBN + НКРЯ/Ngram). Строки `genre-glossary.txt` (создан паком-20, куратор — владелец) 修炼/筑基/修士 — под этим же вопросом.
**Статус: фаза B НЕ стартует** до решений владельца Z1Z4 (рубрика K1K12 + Q1Q6 · вариант по 修炼 · смета B $1.202.60 с пре-регистрацией и критериями провала F1F4 · $0-добор улик перед B). Решения — следующим блоком.

View file

@ -0,0 +1,809 @@
# Полигон, пакет-7, ФАЗА A: терминологическая ресёрч-программа — рубрика, многокнижный майнинг, словарь на подпись
> **Статус: ФАЗА A ИСПОЛНЕНА, СТОП по приёмке.** Промт — `docs/POLYGON_PACKAGE7_TERM_SOURCES_SESSION_PROMPT.md` (v2, выдан 26.07.2026).
> **$0 ПОДТВЕРЖДЁН:** ни одного платного вызова LLM-провайдера. Инструменты фазы: локальный Go/Python + веб-поиск и веб-фетч. Проверка — в §0.4.
> **Зона:** писал только в `eval/pkg7/` и в этот отчёт. `backend/` не тронут (там живая сессия пака-20). Не коммитил.
> **Дальше:** ратификация рубрики §A2, подпись словаря §A4, санкция сметы §A5. До ратификации фаза B не стартует.
> **Ревью-шапка (оркестратор №8, 26.07): ФАЗА A ПРИНЯТА, D39.43.** Приёмка исполнением (5 верификаторов,
> 26 проверок): **24 CONFIRMED · 1 PARTIAL · 1 UNVERIFIABLE · 0 REFUTED.** Воспроизведено ре-раном на пине
> 996bade: gu25 «17 (11/2/4)» и recall 0.071/0.089 (плюс независимый пересчёт recall другим скриптом —
> сошёлся) · gu50 «12» и строка кластера `葛家` с `族长`+`学堂家老` в алиасах ПОБАЙТНО · исчезнувшие между
> 25 и 50 главами — список точен · проба рубрики: все шесть цифр побайтно (включая M3: культивация ←
> {修炼,修行}) · ja под zh-паком: та же восьмёрка, grep 範囲=13/范囲=0, 馬鹿=34/马鹿=0 · en: emitted=0 и
> код тихого продолжения на пине · веб-улики ложной сходимости — дословно (декларация aoimevelho,
> «валюта среди земледельцев», ru-wiki без «культивации», Эксмо «мира совершенствующихся») · квадратичный
> фит, смета 33М токенов и абляция пересчитаны — сходятся · зоны чисты (в `backend/` полигонских следов
> нет, PROGRESS-запись не трёт чужого). Ниты: (1) «вхождений 13/34» — это счёт СТРОК grep -c; вхождений
> 15/35, и freq майнера равен именно им — несущий факт (simp-форма = 0) держится; (2) «~34 строки» словаря —
> на деле 3638, «меньше половины подписываемо» держится. UNVERIFIABLE: «веб-бюджет 200/200» — внутренний
> счётчик той сессии, независимо не проверяется. Решения Z1Z4 — владельцу; фаза B до них не стартует.
## Эхо-блок
```
1. Задача: терминологическая ресёрч-программа A1A5 за $0; фаза B — только ПОСЛЕ ратификации.
2. Зона записи: eval/ + docs/experiments/; отчёт → docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md.
3. backend/ НЕ трогаю (там живая пак-20-сессия: 9 файлов модифицированы, terminology/ и genre-glossary.txt untracked). Не коммичу.
4. Гардрейлы: .env не читать · L3 не обрабатывать · refusal_corpus не открывать · книжный канон ≠ жанровый словарь.
5. A1 веб-обзор+провенанс · A2 рубрика НА РАТИФИКАЦИЮ (веса/спорное — владельцу, не решаю) · A3 многокнижный майнинг
(zh — боевым miner через replace-харнесс вне репо, прецедент пакета-6; en/ja — своими скриптами) · A4 словарь НА ПОДПИСЬ
(улика общепринятости = изданные переводы/словари, не одинокая фан-вики) · A5 пре-регистрация B со сметой.
6. Мандат свободы: своё пре-регистрирую (гипотеза → мера → что опровергнет), кладу отдельной секцией.
7. РОВНО ТАК: ложную сходимость не собирать (leave-one-out, независимость сигналов); самопроверка ИСПОЛНЕНИЕМ —
пере-мерить другим способом; роль терминолога в коде НЕ проектирую.
8. Фаза A завершается СТОПом: отчёт + «чего не покрывает» + подтверждение $0. Заявление = команда.
9. Уже вскрыл встречный сигнал к посылке D39.42 п.1 («культивация» = общепринятое): часть ру-источников зовёт
«культивацию» калькой с английского, а нормой — «совершенствование». Проверяю уликами, а не соглашаюсь.
10. Материал сверен с диском: gu-zhenren(zh) · jinpingmei(zh) · Empire of the Dawn(en, epub) · fifty_shades(en, txt) · isekai_majutsushi(ja, txt).
```
---
## §0. Итог одной страницей
**Пять находок, каждая с сырьём.**
1. **WHICH-канал боевого майнера не видит жанровых терминов — измерено.** Recall против ПОДПИСАННОГО владельцем банка книги: **0.0690.103** (три среза), по классам: `name` 0.231 · `title` 0.0770.154 · **`place` 0.000** · **`term` 0.040**. Самый частый термин книги — 蛊, 8700 вхождений в 50 главах, заглавное понятие романа — **не предлагается никогда**. Механизм — не тюнинг, а конструкция: `emissionEligible` требует типа `name|place|title` и длины ≥2 рун ([miner_emit.go:241-252](../../backend/internal/miner/miner_emit.go#L241-L252)). §A3.3.
2. **Абляция говорит, ЧТО именно стоит recall.** Снятие потолка `emitRankCap=200` → recall 0.089→**0.429**; дополнительно снятие фильтра типа → **0.875**; дополнительно одноиероглифные → **0.911**. Цена: эмиссия 17 → 624 → 13 246 → 15 418 строк при precision спот-чека ≈0.15. То есть **recall в ранжированном МНОЖЕСТВЕ есть, его отдаёт слой ЭМИССИИ** — ровно за подписываемый объём. §A3.4.
3. **Покрытие банка не растёт с длиной книги — оно колеблется и теряет уже найденное.** 10 глав → 15 термов, 25 → 17, 50 → **12**, 100 → 14. `学堂家老` — терм, который владелец подписал на мини-прогоне (D39.37) — на 50 главах исчезает из дельты, будучи проглочен алиас-кластером фамилии `葛家` вместе с родовым титулом `族长`. §A3.5.
4. **Генеральность: движок сегодня физически не может майнить не-ханьские книги, и это ГРОМКО, но с одной тихой дырой.** `lang.Load` для `ja`/`en` падает с явной ошибкой (D39.15 работает). Но если книгу с не-ханьским текстом провести под zh-паком — на английском майнер отдаёт **0 термов и стоп молча авто-продолжает** ([mining.go:70-73](../../backend/internal/pipeline/mining.go#L70-L73)), а на японском отдаёт **8 термов, из них верных 0**, причём эмитируемая строка не существует в книге (`範囲``范囲`, `馬鹿``马鹿`: trad→simp-фолд нормализатора). §A3.6.
5. **Масштаб: майнер квадратичен.** Пять замеров, лог-лог-фит по одной книге: `t ≈ e^-22.23 · chars^2.013`. Экстраполяция на приёмочную книгу (7.78 млн символов) — **≈4.6 часа** одного офлайн-прохода и RSS, растущий с 70 МБ до 365 МБ на 1.9 млн. §A3.7.
**Плюс два ресёрч-результата.**
**§A4 — главный.** Посылка D39.42 п.1 («общепринятое индустрией — „культивация“») **уликами не подтверждается**: разделение проходит по линии «маркетинговые аннотации лицензионных изданий и академия → *совершенствование*» vs «фан-площадки → *культивация/культивирование*». При этом вся «народная» русская глоссарная линия сводится к ОДНОМУ английскому глоссарию и ОДНОМУ русскому блогеру (доказано декларацией самого источника и machine-translation-артефактом «валюта среди **земледельцев**» ← *cultivators*) — то есть «много русских источников согласны» это иллюзия. **Но и обратный вывод я не выдаю за доказанный:** издательская сторона стоит на аннотациях, которые пишет маркетинг и которые жаргона избегают по своей природе; тела изданий не читаны. Поэтому владельцу предлагается **V0 — не подписывать эту строку до одного дешёвого захода** ($0: search-inside по телам двух ISBN + частотный узус НКРЯ/Ngram), и лишь затем V1/V2/V3.
**§A2 — методологический.** Рубрика собрана не с нуля: MQM разводит терминологию на ТРИ разные ошибки (не по глоссарию ≠ непоследовательно ≠ неверно по сути), WMT даёт готовые лемматизированные формулы и — главное — **контроль случайной терминологией**, на котором WMT23 показал, что правильная и случайная подсказки дают похожий прирост. Из-за этого два пункта моего первого наброска протокола ИЗМЕНЕНЫ против улик: плацебо-арм стал «случайный глоссарий» вместо «без глоссария», а скоринг — Best-Worst Scaling вместо абсолютной MQM-шкалы (на литературе MQM ошибочно признаёт ~60% человеческих переводов не лучше машинных, BWS опознаёт человека в 80100%).
### §0.1 Что положено на диск
| Артефакт | Где | Что это |
|---|---|---|
| `split_book.py` | `eval/pkg7/` | нарезка любой книги стенда в общий JSONL-субстрат (zh/ja/en, txt+epub) |
| `minerharness/{main.go,build.sh}` | `eval/pkg7/` | вызов БОЕВОГО `internal/miner` вне репо, `replace` на пин-копию коммита |
| `mine_nonhan.py` | `eval/pkg7/` | мои каналы кандидатов для en/ja (ORTHO · DISP · CONTRAST) |
| `kwic.py` | `eval/pkg7/` | KWIC-контексты (разметка по тексту, а не по памяти; прототип входа терминолога) |
| `seed_recall.py` | `eval/pkg7/` | recall WHICH-канала против подписанного сида |
| `rubric_probe.py` | `eval/pkg7/` | проба ИЗМЕРИМОСТИ критериев рубрики на подписанном сиде |
| срезы, дельты, логи, `recall_*.json` | скретчпад сессии | производные книги — вне git (Р8) |
### §0.2 Как воспроизвести
```bash
./eval/pkg7/minerharness/build.sh <work-dir> 996bade # пин бэкенда + сборка харнесса
eval/.venv/bin/python eval/pkg7/split_book.py --book /home/ubuntu/books/gu-zhenren/guzhenren-utf8.txt \
--rule guzhenren --max-chapters 25 --out <work-dir>/gu25.jsonl
<work-dir>/minerharness/minerharness -in <work-dir>/gu25.jsonl \
-contrast eval/exp16/data/jieba_dict_general_zh.txt \
-langpack <work-dir>/pinned-996bade/backend/configs/langpacks -src zh -tgt ru -out gu25.tsv
eval/.venv/bin/python eval/pkg7/seed_recall.py --seed /home/ubuntu/books/gu-zhenren/guzhenren-seed-v2.yaml \
--chunks <work-dir>/gu25.jsonl --mined-tsv gu25.tsv
```
**Пин обязателен.** Параллельная бэкенд-сессия пака-20 правит рабочее дерево прямо сейчас (`internal/terminology/`, `terminologist.go`, `miner_emit.go` и ещё 7 файлов). `replace` на рабочее дерево дал бы невоспроизводимый прогон и спор «чей это был код». Все цифры отчёта сняты на **996bade**.
> **ВСЕ ссылки `файл:строка` в этом отчёте — по коммиту `996bade`, не по рабочему дереву.** Проверено исполнением: в рабочем дереве те же якоря уже уехали (например, «empty delta, auto-continuing» на 996bade стоит в `mining.go:70-73`, а в рабочем дереве на этих строках уже другой код). Первая редакция отчёта содержала номера строк рабочего дерева — поймано сверкой с пином и исправлено.
### §0.3 Оговорка нормы проекта
`jinpingmei` и `fifty_shades` — претрейн-классика; эмпирика на них **предварительна** до вебновелл-среза. В §A3 это помечено на каждой цифре. `gu-zhenren` и `isekai_majutsushi` — вебновеллы, на них выводы крепче.
### §0.4 Подтверждение $0
Платных вызовов провайдеров ноль: ни один скрипт пакета не импортирует `openai`/`httpx`-клиентов и не читает `*_API_KEY`; `eval/.env` не открывался. Веб-часть — только `WebSearch`/`WebFetch` харнесса сессии (бюджет поиска сессии исчерпан: 200/200 — см. §X). Go-харнесс детерминирован и офлайн по построению (докшапка `miner.go:5-8`: «no LLM, no network, no clock, no randomness»).
---
## §A1. Кабинетный ресёрч: как индустрия и конкуренты ведут терминологию
Метод: шесть независимых углов поиска (издательская практика · конкуренты · zh→ru-улики · ja/en-улики · право и провенанс · литература о метриках), каждый со своим адверсариальным верификатором, которому вменялось **сломать** утверждения ресёрчера, а не подтвердить. Проверялось по 14 самых нагруженных утверждений на угол.
**Сводный вердикт верификации — 84 проверки: 62 CONFIRMED · 14 PARTIAL · 7 MISQUOTED · 1 OVER_ATTRIBUTED · 0 NOT_FOUND · 0 DEAD_URL.**
| Угол | C | P | MISQ | OA |
|---|---|---|---|---|
| Издательская практика (ISO/TBX/CAT) | 9 | 3 | 2 | 0 |
| Конкуренты и платформы | 12 | 2 | 0 | 0 |
| zh→ru улики | 9 | 2 | 2 | **1** |
| ja→ru и en→ru улики | 11 | 2 | 1 | 0 |
| Право и провенанс | 10 | 3 | 1 | 0 |
| Литература о метриках | 11 | 2 | 1 | 0 |
**Ноль NOT_FOUND и ноль DEAD_URL — фабрикации нет.** Целевой дефект (овер-атрибуция) пойман **один раз**, и это ровно тот случай, ради которого проверка ставилась (§A4.1, коллекция Rulate). Три системных класса, вскрытых верификаторами и учтённых по всему отчёту: (1) **цитаты, добытые LLM-суммаризацией страницы, склеиваются в грамматически невозможные фразы** — обязателен второй буквальный проход; (2) **молчаливое усечение квалификатора** — четыре случая в правовом углу, и все четыре режут именно ту оговорку, которая нужна для решения; (3) **обрезанный перечень** читается как исчерпывающий (шестизвенная цепочка ISO 17100 подана пятизвенной). Все несущие утверждения ниже переформулированы под подтверждённую часть цитаты.
### A1.0 Дисциплина улик: где сходимость ЛОЖНАЯ (критик полноты, отдельный проход)
После верификации по отчёту прошёл третий контур — критик полноты, которому вменялось искать дырки, а не подтверждать. **Он снял несколько моих же «сходимостей», и я не оставляю их в тексте в прежнем виде.** Все правки ниже сделаны по его находкам; там, где утверждение осталось, оно помечено уровнем улики.
| Что я назвал сходимостью | Что на самом деле |
|---|---|
| «Три независимых слоя дают 12 полей» (A1.1) | **Полтора слоя.** TBX = ISO 30042, то есть тот же ISO; CAT/TMS обязаны импорт-экспорт TBX и списали модель данных оттуда — доказательство внутри самих цитат (инструменты называют поля TBX-именами: `subjectField`, `partOfSpeech`, тот же четырёхзначный пиклист статуса). Настоящих независимых свидетеля два: ISO-куст и издательский style sheet, причём второй представлен ОДНИМ автором |
| «MQM и ISO 5060 и Google-MQM согласны» (A2.0) | Один куст: ISO 5060 «inspired by MQM», Google-MQM — вариант того же фреймворка тех же авторов. Вывод «правильных весов не существует» опирается на **два варианта одного фреймворка**, а не на эмпирику |
| «Русская школа: Лотте, Гринёв-Гриневич, Суперанская» | Лотте → Гринёв-Гриневич — одна линия преемственности; статьи, из которых взяты формулировки, массово переписывают один канонический абзац. Суперанская — единственный настоящий контр-голос |
| «Палладица общепринята» (A4.2) — ЭКД + ru-wiki + интервью Перловой | **Три пересказа ОДНОГО недоступного документа** (инструкция ГУГК/Концевич), которого ни один из троих не читал. Что вторичный слой уже искажает — видно по атрибуции системы Бичурину вместо Палладия |
| «Поливанов — стандарт де-факто» (A4.5), девять помеченных улик | ru-wiki + один полемист. Смоленский прочитан ВНУТРИ ref-тегов той же ru-wiki, то есть это не второй свидетель |
| «Индустрия пришла к лемме в глоссарии» (A1.2) | **Один вендор — Microsoft.** Архитектурный вывод на одном свидетеле |
**Отдельно — круговая валидация, которую я допустил сам.** В §A2.3 критерий K3 (транскрипционная норма) объявлен «измерим полностью, ноль нарушений на эталоне». Но линейка проверялась против НАШЕГО сида, построенного по НАШЕЙ же таблице Палладия. Ноль нарушений здесь означает «таблица согласна сама с собой», а не «транскрипция верна». Настоящая проверка K3 требует внешнего эталона (изданный перевод или нормативный документ), которого у нас нет.
### A1.1 Обязательные поля терминологической записи
ISO-куст (ISO 12616-1 / TBX=ISO 30042 / инструменты, списавшие модель с TBX) и издательская практика художественной прозы дают ядро из **12 полей**. Это НЕ пересечение трёх независимых традиций (см. A1.0) — это один стандарт плюс один независимый свидетель.
| # | Поле | Улика |
|---|---|---|
| 1 | **Concept ID** (запись = концепт, а не пара строк) | MultiTerm: «MultiTerm is a concept-oriented system, and each termbase entry corresponds to a single concept»; Crowdin: «Concept the highest-level terminology element»; ISO 12616-1 §8.2.2 «Concept orientation» |
| 2 | Definition | TBX-Basic `definition`; Crowdin «A clear explanation of the concept's meaning»; ISO 12616-1 §6.4 |
| 3 | Subject field / домен | TBX-Min `subjectField` («A field of special knowledge»); memoQ «Subject»+«Domain» |
| 4 | Term (обозначение) | TBX-Core `term`; ISO 12616-1 §6.2 |
| 5 | Language | Crowdin, MultiTerm («Language level») |
| 6 | Part of speech | TBX-Min `partOfSpeech`; Crowdin; memoQ; Smartcat |
| 7 | **Status** — закрытый пиклист | TBX-Min: `preferredTerm-admn-sts`, `admittedTerm-admn-sts`, `deprecatedTerm-admn-sts`, `supersededTerm-admn-sts`; Phrase: New → Approved |
| 8 | **Forbidden** (запрещённая форма) | memoQ: «A forbidden term is a word or phrase that shouldn't be used in the translation»; Lokalise: «enable this option if the term must not be used in translations» |
| 9 | Context / usage example | TBX-Basic `context`; memoQ «Example»; Phrase «Usage» |
| 10 | **Source** (откуда взят термин) | TBX-Basic `source` |
| 11 | **Провенанс-аудит: кто/когда** | MultiTerm: «uses a set of four history fields: Created on, Created by, Modified on and Modified by»; memoQ те же четыре; TBX `responsibility`+`transactionType`+`date` |
| 12 | Note | TBX-Core `note`; ISO 12616-1 §6.6 |
**Расхождение слоёв, важное для нас:** в ISO/TBX «запрещённость» — это *значение статуса* (`deprecatedTerm`), а в инструментах — *отдельный булев флаг*. Наш сид сегодня умеет `status: approved|auto`, но **не умеет «запрещено»**а именно этот флаг решает задачу «модель упорно пишет „совершенствование“, а книга живёт на „культивации“».
**Сюрприз издательского слоя.** В художественном книгоиздании эквивалент термбазы зовётся не глоссарием, а **style sheet**, и ведёт его **не переводчик, а копиредактор**: «A style sheet is a document the copyeditor prepares that lists the grammatical conventions, characters, places, unusual or made-up words, and the distinctive treatment of words» (Deanna Hoak, публикация SFWA). Для нас существенны две детали:
- **серийность прямо названа причиной**: «SF/F books often come in series, and a thorough style sheet is important for maintaining continuity from one book to the next» — это ровно архитектурный задел владельца 26.07 «серийный шаринг банка»;
- **поле, которого нет ни в одном ISO/TBX: локатор первого вхождения** — «For all of those, I put in the page number for the first time I saw each item». У нас оно уже есть и называется `since_ch`;
- **и второе поле, которого нет в стандартах: флаг «НЕ унифицировать»** — style sheet используется в том числе чтобы ЗАПРЕТИТЬ нормализацию: «Fragments are acceptable with this author's style». Это прямой аналог нужного нам `do-not-normalize`, и в терминологических стандартах его нет вообще.
*Оговорка:* оба этих поля выведены из ОДНОГО источника (один копиредактор, один пост SFWA). Вес улики — одиночный свидетель, а не практика отрасли.
**Русскоязычный слой — два прямых свидетельства, оба ценные.** Стратегия термина, глава «Истари Комикс» Евгений Кольчугин: «У нас есть некая общая стратегия: **уходить в переводе в хардкор, а не в адаптацию**. То есть лучше оставить оригинальный термин и дать сноску, чем заменить её русским аналогом». И — единственный найденный прямой ответ на вопрос «кто approver» (он про локализацию в Wakanim, не про книги, и это надо держать в уме): «все наши предложения затем пересылаются создателям сериала и даже авторам оригинала, и они уже выбирают и утверждают то название». Там же — что решение по термину принимает команда, а не глава: «Вот тут я могу сказать своё мнение, но не более. Никаких там "я сказал"».
Про смену переводчика в середине серии — переводчик Екатерина Рябова о переходе на «Бакумане» с 13-го тома: «читатели перемены не почувствуют — кроме переводчика и сверщика над книгой работает целый слаженный коллектив». То есть непрерывность обеспечивается **институтом сверщика и редактора**, а документ-глоссарий в интервью не упомянут вовсе.
**PARTIAL-поправка верификатора:** заголовок клаузы 7 ISO 12616-1:2021 — «Text elements for the Term field», а «Designations» это §7.2 (ресёрчер подставил ближайший читаемый подзаголовок — домашний аналог овер-атрибуции). Содержание Annex A по публичному preview непроверяемо: тело вымарано.
### A1.2 Конкуренты: три механизма, у каждого свой класс дефектов
**Главный вывод: задачу «глоссарий для художественного перевода в морфологически богатый язык» не решил никто, и все трое открыто пишут, что гарантий нет.**
**(1) Жёсткая подстановка.** Microsoft Custom Translator сам называет свой словарь «brute force "copy and replace"» и «exact find-and-replace operation», чувствительной к регистру, и там же признаёт главный дефект: при замене фразы теряется контекст предложения и «overall translation quality of the sentence often suffers». Отсюда их же запрет класть в словарь что-либо, кроме составных существительных. Регистр — мина: `sql server` / `sql Server` / `SQL Server` не матчатся к записи `SQL server`. Тот же механизм в фан-инфраструктуре: LunaTranslator подменяет термин плейсхолдером `ZX?Z` до перевода и восстанавливает после; юзерскрипт для LNMTL честно пишет «This is just an advanced text replacer without a translation engine» и что при разрыве/перестановке символов замена не срабатывает.
**(2) Soft-constraint внутри модели.** Amazon Translate: «doesn't guarantee that it will use the target term for every translation». DeepL продаёт это как преимущество («more than a find-and-replace tool»). *Оговорка верификатора:* широко цитируемую фразу DeepL про грамматику и отсутствие нужды во множественных формах подтвердить НЕ удалось (`support.deepl.com` отдаёт 403) — это ровно тот случай, где легко совершить овер-атрибуцию, и мы её не совершаем.
**(3) «Нейрословарь» с леммой — единственное честное решение морфологии, и оно наше.** Microsoft прямо противопоставляет: обычный phrase fix — «An exact find-and-replace operation… in the exact same format»; нейро — «The requested translation **can be inflected or changed casing**» и ожидается «in a **lemma (word base) form**». То есть **один вендор** пришёл туда же, куда пришли бы мы: в глоссарии лежит лемма, склоняет модель. (Улика одиночная — обобщать до «индустрия пришла» нельзя, это поправка критика полноты.) Цена расписана ими же: источник всё равно матчится точно и с учётом регистра (для польского рекомендуют вручную размножать `solution/Solution/solutions/Solutions`); «"As is" copying isn't guaranteed»; «Infrequently (less than 0.1%), a neural phrase fix doesn't respect the… phrase dictionary entry»; механизм **отключается**, если в предложении есть emoji, URL, код, длинные числа. Языковое покрытие нейрословаря: есть `en→ru`/`ru→en`, но **нет ни `zh→ru`, ни `ja→ru`**. *⚠ Расширение «наши пары не покрыты НИКЕМ из большой четвёрки» — проверяемое отрицание, которое НЕ проверено:* страницы поддерживаемых глоссарных пар DeepL / Amazon / Google публичны и не открывались; подтверждено только по Microsoft. Существование отдельного платного продукта «Term Morphology Editor для флективных языков» — прямое свидетельство, что штатно морфология не решена.
**(4) Промпт-инъекция (LLM-эра) и её собственный дефект.** Самый ценный для нас баг-репорт — AiNiee issue #597 (16.05.2025): при автогенерации глоссария «术语表里的翻译和ai实际的翻译有出入» (перевод в глоссарии расходится с фактическим переводом модели), и жёсткий пост-чек выдал **сто тысяч строк ложных ошибок** — «已经完全丧失了正常人工排查的作用了» («полностью утратил смысл ручного разбора»). Это точная модель нашего риска: **автоглоссарий + строковый пост-чек = лавина ложняков.** Наш K6-замер пакета-6 (precision 0.067, 1 подлинный промах против 14 ложных) — тот же дефект в малом масштабе, и мы теперь знаем, куда он растёт.
**(5) Архитектурный дефект, который у нас уже решён иначе.** У LNMTL смена глоссария означает переперевод всей серии: «I have updated glossary upon all of the issued propositions, and queued retranslations of series». D39.42 п.5 («точечная ре-редактура по ключу со сметой») — это прямой ответ ровно на этот дефект конкурента.
*Оговорка о зависимости источников (leave-one-out):* две страницы AWS-гайда несут одно и то же предложение дословно — считать их двумя подтверждениями нельзя.
### A1.3 Провенанс: что законно абсорбировать, что нет, что серо
**Короткий ответ: соответствие «термин → перевод» почти всюду неохраняемо; охраняемы ТАБЛИЦА (подбор и расположение) и ИНВЕСТИЦИИ в базу.**
- **РФ.** ГК ст. 1259 п. 5 выводит из-под охраны идеи, методы, факты; ст. 1260 п. 2 даёт составителю права «на осуществленные ими подбор или расположение материалов». Право sui generis: ст. 1334 п. 1 — «при отсутствии доказательств иного базой данных, создание которой требует существенных затрат, признается база данных, содержащая не менее десяти тысяч самостоятельных информационных элементов». Спасательный круг ст. 1335.1: изготовитель БД «не может запрещать использование отдельных материалов… правомерно полученных использующим их лицом из иных, чем эта база данных, источников».
- **ЕС.** Директива 96/9/EC ст. 7(1) — sui generis при «substantial investment in either the obtaining, verification or presentation of the contents»; ст. 7(5) — запрет «repeated and systematic extraction… of insubstantial parts». CJEU Football Dataco C-604/10: «significant labour and skill… cannot as such justify the protection… if that labour and that skill do not express any originality». Directmedia C-304/07 закрывает лазейку «мы перепечатали руками»: перенос после «on-screen consultation… and an individual assessment» тоже extraction.
- **США.** Feist: «sweat of the brow» отвергнута; «the copyright in a factual compilation is thin»; «a subsequent compiler remains free to use the facts contained in another's publication… so long as the competing work does not feature the same selection and arrangement». Плюс 37 CFR 202.1(a): не охраняются «Words and short phrases such as names, titles, and slogans» — то есть пара «金丹 → золотое ядро» в США не объект охраны в принципе.
**Лицензии типовых источников терминов:**
| Источник | Лицензия | Share-alike | Вердикт |
|---|---|---|---|
| Unihan / Unicode | Unicode License v3, только attribution | нет | **зелёный** |
| Википедия / Викисловарь | CC BY-SA 4.0 + GFDL | да | серый (точечно — можно, выгрузкой — заражает) |
| CC-CEDICT | BY-SA 4.0 (mdbg) vs BY-SA 3.0 (cc-cedict.org) — расхождение | да | серый + UNCLEAR |
| JMdict/EDICT (EDRDG) | CC BY-SA 4.0 | да | серый |
| Fandom/Wikia | CC BY-SA 3.0 Unported | да | **красный** |
| БКРС (bkrs.info) | лицензии нет; сайт: «Базы можно использовать свободно в любых целях» | н/д | серый→красный |
Вирусность измеряется точно: CC BY-SA 4.0 §4(b) — при включении «all or a substantial portion of the database contents» **ваша БД (но не отдельные её элементы) становится Adapted Material**; и контр-правило самого Creative Commons: «use of the facts and ideas embedded within the contents will not require attribution… unless doing so implicates copyright in the database structure». Операционно: **точечный факт из BY-SA источника безопасен, массовая выгрузка заражает нашу базу share-alike.**
#### Предложение правил провенанса (НА РАТИФИКАЦИЮ)
Правило одно и оно процедурное: **доказывать легитимность через год придётся не рассуждением, а записью.** Поэтому каждая строка банка с непустым `dst` обязана нести провенанс из четырёх полей — они ложатся на существующую схему сида без новых сущностей:
| Поле | Значения | Зачем |
|---|---|---|
| `origin` | `model` · `owner` · `genre-pack` · `web` · `series` (round-trip прошлой книги) · `imported-translation` | различает, кто произвёл dst; D39.42 п.«серия»/«импортированный перевод» требуют этого различения |
| `provenance` | свободная строка: URL + дата + класс лицензии | аудит-след; для `web` обязательна |
| `licence_class` | `green` · `grey` · `red` | зелёный — вливаем; серый — точечно и с записью; красный — не вливаем никогда |
| `evidence_kind` | `published-translation` · `academic` · `dictionary` · `platform` · `single-fan-source` | вес улики; **`single-fan-source` не считается уликой общепринятости** |
**Градация с доводом:**
- **Зелёный** — Unicode/Unihan; собственный выход модели в контексте нашей книги (модель произвела dst сама — это не копирование чужой таблицы); подпись владельца.
- **Серый** — единичные факты из BY-SA источников и из БКРС; изданный перевод, процитированный как СВИДЕТЕЛЬСТВО УЗУСА (не как источник для копирования таблицы). Разрешено брать точечно, с записью URL и даты; запрещено выгружать пачкой.
- **Красный** — чужой фан-канон (глоссарии Fandom и фан-групп) и любая массовая выгрузка чужой таблицы. Здесь важно честное различение, которого владелец просил: **это не только правовой запрет, но и редакционный принцип.** Правовая часть слабее, чем кажется (единичное соответствие неохраноспособно), а редакционная — сильная: чужой фан-канон приносит чужие ошибки, чужую транскрипцию и чужие вкусовые решения, и после его абсорбции наша книга перестаёт быть нашей.
**Ограничение, которое надо назвать:** TDM-исключения (EU DSM ст. 3/4 с opt-out, японская ст. 30-4) остались непроверенными первоисточником — угол сорвался вместе с двумя другими вопросами (§X). Правило провенанса от этого не зависит, но раздел «можно ли майнить чужие переводы» в юр-пакет Ф2.5 пойдёт без этой опоры.
---
## §A2. Рубрика «нормально переведённый термин» — ДРАФТ НА РАТИФИКАЦИЮ
**Дисциплина раздела:** веса и спорные критерии я НЕ решаю. Ниже — критерии, шкала, способ измерения и честный статус «чем меряется». Развилки помечены **⟨ВЛАДЕЛЬЦУ⟩**. Рубрика построена НЕ с нуля: сначала собрано, что уже изобретено индустрией и наукой, и лишь потом добавлено своё.
### A2.0 Что уже изобретено (и что нам не надо изобретать)
**MQM разводит терминологию на ТРИ РАЗНЫЕ ошибки, и это главное заимствование.** Категория Terminology определена как несоответствие нормативному отраслевому/организационному стандарту либо неверный нормативный эквивалент; внутри неё — «расхождение с термбазой», «неединообразие» (разные термины для одного понятия) и «неверный термин» (не тот, который употребил бы эксперт домена). *Наша рубрика обязана держать это врозь: «не по глоссарию» ≠ «непоследовательно» ≠ «неправильно по сути».*
**Серьёзность и веса.** MQM Council: `neutral` (преференциальная правка либо приемлемый вариант, помеченный для внимания), `minor`, `major`, `critical` (контент негоден, автоматический Fail); предлагаемые веса экспоненциальные — **0 / 1 / 5 / 25**. Google-версия MQM (WMT) даёт ДРУГИЕ веса и другие подкатегории терминологии («Inappropriate for context», «Inconsistent use»). Вывод, важный для Q1: **«правильных» весов не существует, их выбирают под задачу.**
**Развилка, которую я сам бы пропустил:** MQM требует решить ЗАРАНЕЕ, штрафовать повторяющуюся ошибку каждый раз или только первое вхождение. Для книги, где термин повторяется 200 раз, это решение и есть вся арифметика. Вынесено в Q5.
**ISO 5060:2024** — первый международный стандарт об оценке перевода, включая сырой MT. Его определение ошибки железное и полезное: **«failure to adhere to translation project specifications»**. Его первая категория — Terminology («Inconsistent use of terminology or use of incorrect terms»). *Оговорка о независимости:* ISO 5060 «inspired by MQM», поэтому считать его вторым подтверждением MQM нельзя — это один источник в двух обложках. ASTM WK46396 до сих пор числится work item.
**ISO 704** даёт готовый список принципов образования термина: transparency, consistency, appropriateness, linguistic economy, **derivability**, **linguistic correctness**. Это буквально основание нашего критерия морфологической пригодности.
**WMT Terminology task даёт две готовые формулы и один убийственный контроль.** Формулы: *terminology accuracy* — бинарно по каждому исходному термину, сумма успехов / общее число; *terminology consistency* — доля кандидатов, совпавших с псевдо-референсом (первое вхождение). Для флективных языков они матчат **лемматизированно** — прямое решение нашей проблемы склоняемости. Контроль: три режима — без терминологии, с правильной и **со случайной**. И находка WMT23: **правильная и СЛУЧАЙНАЯ подсказки дали похожий прирост по метрикам качества.** Это лучший из найденных аргументов, что замер терминологии без случайного плацебо-арма — самообман. Наш P0 (§A5.2) поэтому переопределён.
**Слепые пятна автометрик и провал MQM на художественном тексте.** Автометрики «insensitive to nuanced differences in translation quality… most pronounced when the quality is high» — то есть глухи ровно там, где мы работаем. Хуже: на литературе при MQM-оценке ~60% ЧЕЛОВЕЧЕСКИХ переводов ошибочно признаются не лучше машинных, тогда как простой **Best-Worst Scaling опознаёт человека в 80100%**. Литературная рубрика LiTransProQA формулирует терминологический пункт иначе: «Have I maintained consistency in terminology, character names, slang, dialect throughout the text?». Реалистичный потолок человеческого согласия на литературных фреймворках — **Krippendorff α 0.690.79**.
**Согласие и слепота — цифры, а не пожелания.** LandisKoch: 0.600.80 «Substantial», 0.801.00 «Almost Perfect». Практика WMT: межаннотаторский Kendall τ-c у ESA 0.254 против MQM **0.116** — реальное согласие людей на MQM низкое. При фиксированном бюджете Google рекомендует **один рейтер на элемент, но больше элементов**. LLM-судья: self-enhancement (GPT-4 завышает себе ~10%, Claude ~25%), мультиязычно Fleiss κ ≈ 0.3. И свежая работа 2026 ломает наш собственный рефлекс: **позиционный свап на кураторских бенчмарках УХУДШАЕТ (4…13 п.п.)**, доминирует style bias.
**⚠ Уровень улики по этому подразделу — назван честно, потому что на нём стоят наши критерии.** Дословно из первоисточника получены: определения трёх типов терминологической ошибки MQM, определение `neutral`, требование решить про повторяющиеся ошибки, формулы WMT, эффект WMT23 «правильная ≈ случайная», данные по MQM на литературе и по BWS, цифры согласия. **НЕ получены дословно и держатся на заголовках/пересказах:** веса 0/1/5/25 (шли как аннотация вне кавычек); клауза 6 ISO 5060 с семью категориями и порогами (вторичные пересказы, включая сайт, который не ISO); принципы ISO 704 (`derivability`, `linguistic correctness`) — заголовок + переформулировка терминолога в блоге, тело стандарта не читано; пороги Krippendorff 0.690.79. **Следствие, которое надо знать:** наши новые критерии K5 и K5b легитимированы ссылкой на ISO 704, тело которого мы не читали — это ровно тот паттерн овер-атрибуции, за которым мы охотимся у других. Критерии остаются (они разумны сами по себе), но **ссылка на ISO 704 — не доказательство, а указание, куда смотреть.**
**Русская терминологическая школа** даёт то, чего нет в MQM. Лотте: «фиксированное содержание (определенность), точность, однозначность, отсутствие синонимов, краткость». Гринёв-Гриневич: соответствие нормам языка, краткость, **деривационная способность**, мотивированность, систематичность. И прямой контраргумент против краткости — Суперанская: **«точность в нем важнее краткости»**. СПР: «Во всем тексте перевода должно быть соблюдено единство терминологии». Отраслевой SAE J2450 содержит готовую пару категорий: «Wrong Term» (minor 2 / serious 5) и **отдельную** «Word Structure or Agreement Error» (2/4) — то есть морфология штрафуется в индустрии отдельной категорией, а не внутри терминологической.
### A2.1 Критерии
Стартовый набор промта принят целиком, разложен по трём MQM-типам и расширен пятью позициями (K8K12). Колонка «откуда» — чьё это, чтобы не выдавать заимствование за своё.
**Как читать колонку «шкала».** Машинные критерии (K3, K4, K8, K10, K12) дают абсолютное значение — там шкала и есть результат. **Судейские критерии (K1, K2, K6, K7, K9, K11) в фазе B абсолютными баллами НЕ выставляются**: по каждому из них оценщик выбирает лучший и худший вариант среди армов (Best-Worst Scaling, §A2.4 п.3), а колонка «шкала» здесь описывает только определение критерия и остаётся для катастроф-экрана. Это сделано против улики о провале абсолютной MQM-шкалы на художественном тексте, а не для удобства.
| # | Критерий | Что значит «хорошо» | Шкала | Чем меряется | Откуда |
|---|---|---|---|---|---|
| **K1** | **Верность концепту** («тот ли термин») | dst передаёт то же понятие, что src в ЭТОЙ книге | 0/1/2 | человек или LLM-судья по KWIC | MQM *wrong term* |
| **K2** | Жанровая конвенция | dst совпадает с ру-узусом жанра | 0/1/2 | сверка с жанровым словарём §A4 | MQM *расхождение с термбазой* |
| **K3** | Транскрипционная норма | онимы — Палладий (zh) / Поливанов (ja) | 0/1 | **машинный**: таблицы langpack + фонотактика | наше + §A4.2/A4.5 |
| **K4** | **Единообразие в книге** | один концепт — один термин во всём тексте | 0/1 | **машинный, ЛЕММАТИЗИРОВАННО** | MQM *inconsistency* + WMT *consistency* + СПР |
| **K5** | Морфологическая пригодность | dst склоняем, род устойчив, согласование корректно | 0/1/2 | **гибрид** — см. A2.3 | ISO 704 *linguistic correctness* + SAE J2450 *Word Structure* |
| **K5b** | **Деривационная способность** (новое) | от dst образуются нужные производные и композиты | 0/1 | полумашинный (парадигма + композиты) | ISO 704 *derivability* + Гринёв-Гриневич |
| **K6** | Литературность/благозвучие | dst годится в художественный текст | 0/1/2 | только человек | Гринёв-Гриневич *эвфония*; MQM `neutral` |
| **K7** | Спойлер-безопасность | dst не раскрывает того, чего в этой точке не знают | 0/1 | человек + машинный экран по `since_ch` | наше |
| **K8** | **Однозначность банка** | нет коллизий «один dst ↔ разные src» и наоборот | 0/1 | **машинный** (реализован, A2.3 M3) | Лотте *отсутствие синонимии* |
| **K9** | **Уместность регистра** | академический термин не подставлен в жанровый текст и наоборот | 0/1/2 | человек | наше (из §A4) |
| **K10** | **Провенанс-полнота** | у dst записаны origin/provenance/licence_class | 0/1 | **машинный** | наше (из §A1.3) |
| **K11** | **Сокращаемость** | у длинного dst есть работающая краткая форма | 0/1 | человек | D39.39 Р3 |
| **K12** | **Term success rate** (агрегат) | доля термов книги, доехавших до финала в правильной форме | доля | **машинный, лемматизированно** | WMT25 *terminology accuracy* |
**Обоснование добавленных.** K5b и K12 — прямые заимствования (ISO 704 / WMT25), их не было в стартовом наборе, и они дешёвые. K8 — коллизия найдена живьём на подписанном сиде (§A2.3). K9 — §A4 показал, что академическое «золотой эликсир» и жанровое «золотое ядро» это РАЗНЫЕ традиции. K10 — следствие §A1.3. K11 — вопрос Р3 из D39.39, переведённый из спора в критерий.
**Критерий, который я НЕ включаю, и довод:** «краткость» (Лотте, ISO 704 *linguistic economy*). Она измерима тривиально (длина), но прямо оспорена Суперанской («точность в нем важнее краткости») и на художественном тексте конфликтует с K1 и K6. Если владелец хочет — включается одной строкой, но по уликам это не бесспорный критерий, а предпочтение.
### A2.2 Шкала серьёзности и катастроф-экран
**Шкала — заимствованная, не выдуманная:** `neutral` (приемлемый вариант, помечен для внимания) · `minor` · `major` · `critical` (автоматический Fail). Веса MQM Council по умолчанию **0 / 1 / 5 / 25**; Google-версия использует другие. ⟨ВЛАДЕЛЬЦУ⟩ — Q1.
Ни один балл по K1K12 не компенсирует катастрофу. Экран бинарный (`critical`), и он применяется **и к победителю тоже** (урок exp12/13):
- **C1** — dst меняет смысл на противоположный или несовместимый (класс «шкала сломана в 10 раз» из пакета-6);
- **C2** — dst раскрывает сюжетный поворот раньше книги;
- **C3** — dst нарушает подписанный канон книги (`CANON-NOTES.md`);
- **C4** — dst внесён из красного источника (§A1.3).
### A2.3 Проба ИЗМЕРИМОСТИ — исполнена, не декларирована
Рубрика без работающей измерялки — мнение. `eval/pkg7/rubric_probe.py` прогнан по **подписанному владельцем сиду** `guzhenren-seed-v2.yaml` (58 термов с dst):
```
M1 разобрано pymorphy3: 58/58; вершина УГАДАНА предсказателем: 14
M1 объявлены формы в сиде: 42; СОВПАЛИ с порождёнными: 18 (0.429)
M1 конфликт «invariant vs склоняемость», вершина СЛОВАРНАЯ: 10
M2 нарушений (латиница/цифра/фонотактика Палладия): 0
M3 один src → несколько dst: 0
M3 один dst → несколько src: 1 {'культивация': {'修炼', '修行'}}
```
Читается так:
- **K3/M2 — измерим бесплатно, но ЭТА проверка круговая.** Ноль нарушений на эталоне означает, что линейка не стреляет по чистым данным — и только. Сид строился по той же таблице Палладия, которой линейка проверяет: это согласие таблицы с самой собой, а не доказательство верности транскрипции (поправка критика полноты, §A1.0). Настоящая проверка K3 требует внешнего эталона — изданного перевода той же книги или нормативного документа ГУГК/Концевича, и ни того, ни другого у нас пока нет.
- **K8/M3 — измерим полностью и сразу нашёл настоящую коллизию:** `修炼` и `修行` в подписанном сиде имеют ОДИН dst «культивация». Для терминолога это ровно тот случай, где обратное отображение неоднозначно, а для §A4 — сигнал, что различение 修炼/修真/修行 у нас пока стёрто.
- **K5/M1 — измерим только как ЭКРАН, не как арбитр.** pymorphy3 разбирает вершину у всех 58, но у 14 — предсказателем (транслитерированные онимы: «Чилянь» разбирается как глагол, порождая «чилянувшего»); и он не согласует зависимые («первый ранг» → «первого ранг» вместо «первого ранга»). Поэтому: автомат годится, чтобы поймать «объявлено неизменяемым, а по словарю склоняется», и НЕ годится, чтобы проверять рукописные `decl.forms`. Цифра 0.429 — это НЕ доля ошибок владельца, это доля случаев, где автомат вообще способен вынести суждение.
- **Собственная ошибка, найденная исполнением:** первая версия пробы брала вершиной последний токен и на «Монах Цветочного Вина» склоняла «Вина» — согласие вышло 0.238 по МОЕЙ вине. После правила «у имени вершина последняя, у нарицательного словосочетания первая» — 0.429. **Требование из этого:** правило вершины зависит от типа терма, и любой морфо-чекер обязан его знать.
### A2.4 Слепой протокол оценки
Протокол переписан против литературы §A2.0, а не только против нашего опыта. Три пункта изменились именно потому, что улики противоречили моему первому наброску, и это отмечено явно.
1. **Единица оценки — терм×книга**, не чанк: терминолог решает про терм.
2. **Что видит оценщик:** src · 58 KWIC-контекстов из исходника (`kwic.py`) · список вариантов dst в **перемешанном** порядке. Что НЕ видит: какой арм/промт произвёл вариант, какой вариант чей, есть ли среди них подписанный.
3. **Способ скоринга — Best-Worst Scaling, а не абсолютные баллы MQM. ⟵ ИЗМЕНЕНО против первого наброска.** Довод — улика, а не вкус: на литературном материале при MQM-оценке ~60% человеческих переводов ошибочно признаются не лучше машинных, тогда как BWS опознаёт человека в 80100%. Абсолютная MQM-шкала остаётся только для катастроф-экрана (`critical`), где она бинарна и не требует градаций.
4. **Плацебо-арм — СО СЛУЧАЙНОЙ терминологией, а не «без терминологии». ⟵ ИЗМЕНЕНО.** WMT23 показал, что правильная и случайная терминологические подсказки дают ПОХОЖИЙ прирост по метрикам качества; значит арм «без глоссария» ничего не доказывает, а арм «со случайным глоссарием» — доказывает. Дополнительно среди вариантов держим подписанный владельцем dst (где он есть): оценщик, не отличающий подписанный вариант от случайного, отбраковывается вместе со своими вердиктами.
5. **Соль перемешивания — по терму** (не по прогону): урок D13.5, иначе метка утекает через порядок. **Оговорка:** позиционный свап на кураторских наборах, по работе 2026 г., может УХУДШАТЬ результат (4…13 п.п.) при доминирующем style bias. Поэтому свап делаем, но эффект свапа измеряем отдельно и не считаем его бесплатным.
6. **Согласие:** считаем **пер-критерий**, а не в среднем — «в среднем сходится» скрывает расхождение на несущем критерии. Каждое мульти-сигнальное утверждение проходит **leave-one-out по оценщику и по критерию**. **Целевой уровень назван заранее:** Krippendorff α 0.690.79 как ориентир литературных фреймворков; ниже 0.6 результат не выдаётся (F4). *⚠ Оговорка: эти пороги взяты из вторичного изложения, первоисточник не открыт* — то есть порог F4 сегодня держится на непроверенной цифре, и если владелец захочет строгости, его надо либо подтвердить, либо назначить волевым решением. Напоминание из литературы: реальное межаннотаторское согласие на MQM низкое (Kendall τ-c 0.116), поэтому высокого α по абсолютной шкале ждать не следует — ещё один довод за BWS.
7. **Бюджетное правило — один оценщик на элемент, но больше элементов** (рекомендация Google при фиксированном бюджете), а не три оценщика на малую выборку.
8. **Судьи:** ≤2 семейства, судья не судит выход своего семейства (D13.3); риг-стандарт D39.7 (per-vote, полно-evidence, floor-гейт). Известные смещения LLM-судьи держим в уме количественно: self-enhancement до 1025%, мультиязычный Fleiss κ ≈ 0.3.
9. **Порядок:** сначала машинные критерии (K3/K4/K8/K10/K12 — $0, матчинг ЛЕММАТИЗИРОВАННЫЙ по рецепту WMT), и только термы, прошедшие машинный слой, идут к человеку/судье. Это прямо снижает смету фазы B.
### A2.5 ⟨ВЛАДЕЛЬЦУ⟩ — что решать
- **Q1. Веса и шкала.** Брать ли экспоненциальные веса MQM Council (0/1/5/25) или свои? Улика говорит, что «правильных» весов нет — их выбирают под задачу. Мой довод, не решение: K1 и катастроф-экран должны доминировать; K6 (благозвучие) — самый дорогой и самый спорный, его вес определяет, нужен ли человек в петле вообще.
- **Q2. K2 против K1.** Что делать, когда жанровая конвенция и смысловая верность расходятся (жанровое «золотое ядро» vs академический «золотой эликсир»)? Это выбор регистра издания, не измерение.
- **Q3. Единица «хорошо».** Порог приёмки терма — все критерии ≥1, или взвешенная сумма ≥ порога? Первое строже и проще защищать.
- **Q4. K11 и Р3 из D39.39.** Считаем ли сокращённую форму отдельной строкой банка (со своим dst) или полем существующей? От ответа зависит и рубрика, и precision K5c/K6 в паке-21.
- **Q5. Повторяющаяся ошибка — штрафуем каждое вхождение или только первое?** MQM требует решить это ЗАРАНЕЕ. Для книги, где термин повторяется сотни раз, это решение и есть вся арифметика оценки: при «каждое вхождение» один неверно переведённый частотный терм топит книгу, при «только первое» — теряется вес систематической ошибки.
- **Q6. Включаем ли «краткость» в рубрику** (Лотте/ISO 704) при том, что Суперанская прямо ставит точность выше краткости, а на художественном тексте краткость конфликтует с K1/K6.
---
## §A3. Многокнижный майнинг: что есть на самом деле
### A3.1 Материал и субстрат
| Книга | Язык / жанр | Взято | Чанков | Символов |
|---|---|---|---|---|
| 蛊真人 (gu-zhenren) | zh, сянься-вебновелла | главы 110 / 25 / 50 / 100 | 123 / 305 / 613 / 1221 | 192 711 / 471 496 / 951 774 / 1 921 059 |
| 金瓶梅 (jinpingmei) | zh, классика (претрейн) | весь ctext-срез, 12 回 | 66 | 102 669 |
| 異世界魔術師 (isekai) | ja, исэкай-вебновелла | 41 話 | 138 | 226 257 |
| Empire of the Dawn | en, тёмное фэнтези (epub) | 122 документа spine | 958 | 1 589 870 |
| Fifty Shades of Grey | en, совр. роман (претрейн) | 26 глав | 446 | 814 756 |
Субстрат общий: `{chapter, chunk_idx, source}` — ровно то, что `pipeline/mining.go:52-55` подаёт в `miner.Chunk`. Нарезка НЕ боевая (боевой чанкер режет по output-бюджету модели); отклонение пре-регистрировано и **замерено** — см. A3.9.
### A3.2 Прогоны боевого майнера (пин 996bade)
| Срез | Символов | Время | RSS | Эмитировано | По типам |
|---|---|---|---|---|---|
| jpm 12回 | 102 669 | 3.74 с | 70 МБ | 23 | name 21 · place 2 |
| gu 10гл | 192 711 | 10.83 с | 86 МБ | 15 | name 11 · place 2 · title 2 |
| gu 25гл | 471 496 | 50.47 с | 125 МБ | 17 | name 11 · place 2 · title 4 |
| gu 50гл | 951 774 | 212.09 с | 298 МБ | **12** | name 9 · place 2 · title 1 |
| gu 100гл | 1 921 059 | 1124.65 с | 365 МБ | 14 | name 8 · place 3 · title 3 |
Сид при прогоне ПУСТОЙ — иначе замер вырождается: `miner_emit.go:129` исключает засеянные поверхности по построению.
### A3.3 Находка 1: recall WHICH-канала против подписанного банка
`seed_recall.py`, знаменатель — только термы сида, физически встречающиеся в срезе:
| Срез | Термов сида в срезе | Предложено как терм | Как терм алиас | name | title | place | term | nickname |
|---|---|---|---|---|---|---|---|---|
| gu 10гл | 53 | 4 (**0.075**) | 4 (**0.075**) | 0.250 | 0.077 | 0.000 | 0.000 | 0.000 |
| gu 25гл | 56 | 4 (**0.071**) | 5 (**0.089**) | 0.231 | 0.077 | 0.000 | 0.043 | 0.000 |
| gu 50гл | 58 | 4 (**0.069**) | 6 (**0.103**) | 0.231 | 0.154 | 0.000 | 0.040 | 0.000 |
Не предлагается НИКОГДА, при таких частотах в 50 главах: `蛊` 8700× (заглавное понятие книги) · `转` 2171× · `蛊师` 1429× · `蛊虫` 978× · `真元` 471× · `古月` 451× (родовое имя главного героя!) · `修行` 326× · `南疆` 189×.
**Механизм — код, а не тюнинг** ([miner_emit.go:241-252](../../backend/internal/miner/miner_emit.go#L241-L252)):
```go
func emissionEligible(c scoredCand, subsumed, seedSurfaces map[string]bool, pack *lang.Pack) bool {
if !hasAnyType(c.Types, "name", "place", "title") { return false } // ← «term» не эмитируется НИКОГДА
if c.Freq < emitMinFreq || subsumed[c.Src] || runeLen(c.Src) < 2 { return false } // / отсечены длиной
if isFragment(c.Src, seedSurfaces, pack) { return false }
return true
}
```
Тип присваивает только паттерн-канал (фамилия/топо-суффикс/титул-суффикс). У доменного понятия паттерна нет ⇒ `Types` пуст ⇒ строка не эмитируется. **Это не баг: докшапка `miner.go:20-24` прямо говорит, что инварианты — это множество кандидатов, recall@proposed и катастроф-экран, а dst доставляет банкнота.** Но следствие для пака-20 надо назвать вслух: **вход терминолога, собранный сегодняшним WHICH-каналом, почти не содержит жанровых терминов** — 3%-разъезд каналов D39.42 в разрезе по классам выглядит как 0.23 на именах и 0.04 на терминах.
### A3.4 Находка 2: абляция — сколько recall стоит каждый фильтр
Ablation на пин-копии (правки только в скретчпаде, репозиторий не тронут), срез gu 25 глав, 56 термов сида:
| Вариант | Что снято | Эмитировано | recall (терм∪алиас) | term | title | place | name |
|---|---|---|---|---|---|---|---|
| **A** — как в проде | — | 17 | **0.089** | 0.043 | 0.077 | 0.000 | 0.231 |
| **B** | потолок `emitRankCap = 200` | 624 | **0.429** | 0.087 | 0.846 | 0.800 | 0.462 |
| **C** | B + фильтр типа `name\|place\|title` | 13 246 | **0.875** | 0.870 | 0.923 | 0.800 | 0.846 |
| **D** | C + фильтр длины `runeLen < 2` | 15 418 | **0.911** | 0.913 | **1.000** | 0.800 | 0.846 |
Precision спот-чек по варианту C (детерминированная выборка 30 строк, сид 20260726, разметка моя, один разметчик): годными выглядят ~46 из 30 (`蛊群`, `古月赤城`, `浪迹天涯`, `云土`, `议论声`), остальное — грамматические огрызки (`便是`, `有多`, `的关`, `分之一`, `动的`, `则是一`). **Precision ≈ 0.15**, и это спот-чек, а не измерение.
**Что это значит для архитектуры терминолога.** Полный банк книги ДОСТУПЕН — он лежит в ранжированном множестве и достаётся снятием двух констант. Не доступна была **подписываемость**: 15 418 строк владелец подписать не может, 17 — может. Роль терминолога меняет именно эту экономику: консолидировать 13k кандидатов модель может за центы, а подписывать их человеку не нужно (авто-режим D39.42 п.3). **То есть у D39.42 появилось количественное основание, которого раньше не было: потолок покрытия — не свойство детектора, а свойство слоя эмиссии, и он снимается.** Чего это стоит — отдельный вопрос сметы: 13k кандидатов × KWIC-контексты это уже не «центы/книга» (см. A5.5).
### A3.5 Находка 3: покрытие не растёт с длиной книги и теряет уже найденное
10гл → 15 термов, 25гл → 17, 50гл → **12**, 100гл → 14. Исчезнувшие между 25 и 50 главами: `凤金煌`, `天鹤上人`, `学堂家老`, `石人`, `百家族长`, `李小白`, `白骨山`, `葛谣`(частично).
Механизм виден в самой дельте gu50:
```
葛家 name 357 1 学堂家老|家族长|家老|族长|百家族长|老族长|葛家老族长 surname:葛|formant_prefix:葛
```
Алиас-кластер фамилии `葛家` («род Гэ») проглотил **родовой титул `族长`** («глава клана») и **`学堂家老`** («старейшина школы») — титул, принадлежащий вообще другому клану (古月), и именно тот терм, который владелец подписал на мини-прогоне 26.07 с dst «старейшина школы» (D39.37). Проверено по тексту через `kwic.py`:
```
[гл.1/0] …古月族长中年模样,两鬓微霜,一身素白庄重的祭祀服装… ← 族长 при клане 古月
[гл.21/0] …我是葛家部族的人,名叫葛谣,我的阿爸就葛家的族长… ← 族长 при клане 葛家
[гл.4/0] …负责此行的,是学堂家老。他须发皆白… ← 学堂家老, клан 古月
```
`族长` — общеродовой титул, встречающийся при разных кланах; склеивать его в сущность одной фамилии неверно. **Это точечный дефект кластеризации с адресом и воспроизведением**, и он стоит владельцу подписанного терма.
### A3.6 Находка 4: генеральность — ja и en
**Громкая часть (работает как задумано):** `lang.Load(root,"ja","ru")` и `(...,"en","ru")` падают:
```
langpack "ja-ru": open .../langpacks/ja/surnames-single.txt: no such file or directory
(add the file or fix the book's source_lang/target_lang; D39.15: language data is required, never silently empty)
```
То есть **не-ханьская книга сегодня просто не конфигурируется** — обещание D39.15 «никогда молча пусто» исполняется.
**Тихая часть (дыра):** если не-ханьский текст провести под ZH-паком (мисконфигурация или переиспользование пары), майнер не отказывает:
| Книга | Эмитировано | Верных | Что вышло |
|---|---|---|---|
| Fifty Shades (en) | **0** | — | пустая дельта ⇒ `mining.go:70-73` пишет INFO «empty delta, auto-continuing» и стоп **молча** пропускает книгу |
| Empire of the Dawn (en) | **0** | — | то же |
| 異世界魔術師 (ja) | **8** | **0** | все восемь — обычная японская лексика, опознанная как китайские фамилии |
Японские восемь целиком:
```
何処 name surname:何 ← «где» 何故 name surname:何 ← «почему»
元々 name surname:元 ← «изначально» 危険 name surname:危 ← «опасность»
王国 name surname:王 ← «королевство» 王女 name surname:王 ← «принцесса»
范囲 name surname:范 ← «диапазон» 马鹿 name surname:马 ← «дурак»
```
И отдельная мина: **эмитированная строка не существует в книге.** `text.NormalizeSourceKey` фолдит trad→simp, поэтому 範囲 (13 вхождений) выходит как `范囲` (0 вхождений), 馬鹿 (34) — как `马鹿` (0). Проверено `grep -c` по исходному файлу. Для японской пары такой ключ не найдёт себя ни в инъекции, ни в пост-чеке.
**Мои каналы для en/ja** (`mine_nonhan.py`, три независимых канала, считаются раздельно):
- **en, ORTHO+DISP+CONTRAST** (опора — вторая английская книга стенда, кросс-книжный контраст). Верхушка Empire of the Dawn: `dior · gabriel · aaron · jean-françois · voss · grail · phoebe · león · maryn · reyne · empress · marquis · redeemer · unbound · ashdrinker · silversaint`. Многословные, которых боевой канал не может произвести в принципе: `Forever King · Last Silversaint · Iron Maiden · San Michon · Empress of Wolves and Men · Holy Grail of San Michon · Cathédrale de Lumière · Fivefold Throne · Tower of Tears · Petit Monstre`. У Fifty Shades тот же канал вытащил **жанровые ролевые термины** `dominant`(123×, 6 глав) и `submissive`(120×, 4 главы) — они капитализованы в тексте как роли, и это редкий случай, когда жанровое понятие ловится орфографией.
- **ja, канал катаканы** — 65 кандидатов, качество высокое без всякого контраста: имена (`ヤード · ソフィ · ティア · フェアリス · ナタリア`) и жанровые расы/реалии (`エルフ · ハイエルフ · ダークエルフ · ウッドエルフ · ドラゴン · メイド`). Боевому майнеру катакана невидима полностью.
- **ja, канал «одинокой строки»** — 30 кандидатов, и это **самый ценный класс книги**: `記憶閲覧 · 記憶抽出 · 窃思 · 支配 · 上級治癒 · 上級転移 · 上級集団転移 · 思考誘導 · 遮音結界 · 術式暴走 · 術式消去 · 隠密 · 施錠 · 解呪 · 誓約`. Это названия техник магической системы. В дампе syosetu они уплощены из руби/эмфазы в отдельные короткие строки — **разметка исходника несёт терминологический сигнал, и мы её выбрасываем.** Частоты этих термов **210**, то есть они **ниже боевого порога `emitMinFreq = 5`** и ниже `FreqFloor = 3` для половины списка. Класс, который в книге важнее всего, по частоте самый бедный.
- **ja, канал кандзи** отдан боевому майнеру: моя первая версия пыталась контрастировать кандзи против jieba-словаря и получила вырожденное ранжирование (для OOV-слова лапласова вероятность постоянна ⇒ «над-представленность» = log частоты ⇒ наверх всплыли 彼女/見/気). Ошибка моя, найдена исполнением, канал понижен до инвентаря.
### A3.7 Находка 5: масштаб квадратичен
Лог-лог-фит по четырём срезам ОДНОЙ книги (вторая книга в фит не смешана):
```
t ≈ exp(-22.231) · chars^2.013
192 711 симв. → факт 10.83 с / модель 9.59 с
471 496 → факт 50.47 с / модель 58.06 с
951 774 → факт 212.09 с / модель 238.69 с
1 921 059 → факт 1124.65 с / модель 981.09 с
```
Показатель **2.013** — чистая квадратичность. Экстраполяция на приёмочную книгу (7.78 млн символов): **≈4.6 часа** одного офлайн-прохода майнинга, RSS по тренду — единицы гигабайт. Это не деньги, это время и память, и это **вход в решение о МАСШТАБЕ** (D39.33: масштаб последним). Экстраполяция помечена как экстраполяция: замеров выше 1.9 млн символов нет.
Кандидаты источника квадратичности видны в коде и НЕ проверены исполнением (профиля не снимал): `subsumedCandidates` перебирает по длинам все пары кандидатов ([miner_detect.go:80-103](../../backend/internal/miner/miner_detect.go#L80-L103)); `computeCValue` для каждого кандидата перечисляет все его подстроки ([miner_detect.go:38-75](../../backend/internal/miner/miner_detect.go#L38-L75)); `countOccurrences`/`entitySinceCh` сканируют весь текст на кандидата.
### A3.8 Находка 6: классы терминов по языкам и жанрам
Сводка по всем пяти книгам. Колонка «где живёт» — ответ на вопрос промта «что жанровое ↔ что книжное ↔ что серое».
| Класс | zh сянься | zh классика | ja исэкай | en тёмное фэнтези | en совр. роман | Где живёт |
|---|---|---|---|---|---|---|
| Личное имя | 方源, 白凝冰 | 李瓶儿, 王婆 | ヤード, ソフィ | Dior, Gabriel | Christian, Ana | **книжное** |
| Родовое/клановое имя | 古月, 葛家 | — | ウェルナ | Chastain, Voss, House Augustin | Grey, Steele | **книжное** |
| Топоним | 南疆, 青茅山 | — | イストリア | San Michon, Dún Maergenn | Seattle, Escala | книжное (реальные — жанровое) |
| Организация/секта | 仙鹤门 | — | 魔帝国 | Silver Order, Endless Legion | Grey Enterprises Holdings Inc | книжное |
| **Титул/ранг** | **族长, 家老, 蛊师, 一转…六转, 甲等…丁等** | 千户, 大舅 | 司教, 貴族 | Marquis, Empress, Chevalier, Redeemer | Miss, Sir | **ЖАНРОВОЕ** |
| **Жанровое понятие системы** | **蛊, 修炼, 真元, 空窍, 资质, 元海** | — | **術式, 魔導, スキル** | Grail, Silversaint, Unbound | **Dominant, Submissive** | **ЖАНРОВОЕ** |
| Техника/приём | 春秋蝉, 炼化 | — | **記憶閲覧, 遮音結界, 上級転移** | Sainted Blade | — | книжное (шаблон — жанровый) |
| Артефакт/предмет | 月光蛊, 元石 | — | オーブ | Ashdrinker, Moonsthrone | Charlie Tango, Red Room of Pain | книжное |
| Существо/раса | 妖, 魔 | — | **エルフ, ハイエルフ, ドラゴン** | — | — | **ЖАНРОВОЕ** |
| Единица меры/времени | 时辰, 转 | 钱银子 | — | — | — | **ЖАНРОВОЕ** (пара) |
| Обращение/гоноратив | 娘子, 姑子 | 妈妈, 大姐 | **-様, ご主人様** | Mlle, Mother | Miss/Mr | **ЖАНРОВОЕ** (пара) |
**Три вывода из таблицы.**
1. **Жанровые классы — это ровно те, которые боевая эмиссия не отдаёт** (титулы 0.0770.154, понятия системы 0.040, существа/расы — вне ханьского канала). Книжные классы (имена) она отдаёт лучше всех (0.231). Разделение труда напрашивается само: **имена — майнеру, жанровые классы — langpack-словарю и терминологу.**
2. **«Серое» существует и его надо назвать:** титул `族长` жанровый, а `四代族长` («Четвёртый глава рода») — книжный; раса `エルフ` жанровая, а `ハイエルフ`/`ダークエルフ` — на границе (общежанровые, но набор варьируется книгой); `золотое ядро` жанровое, а какое именно ядро у героя — книжное. Правило, которое я предлагаю: **жанровое = встретится в другой книге того же жанра без изменения смысла.** Проверяемо: терм жанровый, если он есть минимум в двух книгах жанра.
3. **Классов, которых наш сид сегодня не различает, два:** «единица меры» (сидит в `term`, хотя ведёт себя как `title`/системное) и «обращение/гоноратив» (в zh сидит в `title`, в ja потребует своего механизма — это вход пака-19 «ты/вы», а не терминолога).
### A3.9 Самопроверки исполнением
Мандат 12.07 требует пере-мерить своё другим способом. Сделано четыре проверки, три из них поймали МОИ ошибки.
1. **Чувствительность к нарезке (главная — она валидирует весь §A3).** Гипотеза: моя нарезка не боевая, значит могла сдвинуть результат. Пере-мерил свипом целевого размера чанка 800 / 2000 / 6000 символов на gu10: эмитированное множество **побайтно одинаково**, Жаккар 1.000, |A∩B∩C| = |ABC| = 15. **Отклонение субстрата на результат не влияет** — цифры §A3 не артефакт нарезки.
2. **KWIC-проверка разметки.** Все спорные кандидаты 金瓶梅 проверены по тексту, а не по памяти. `应伯爵山` оказался не дефектом майнера, а **артефактом исходного файла**: в ctext-срезе заголовки обрезаны многоточием («应伯爵山... :»), и майнер честно нашёл частую строку. `明日` («завтра») и `钱银子` («…цянь серебра») — настоящие ложняки фамильного паттерна. `玉楼` — сущность верна (孟玉楼, персонаж), тип неверен (`place` через topo_suffix:楼). Алиасы `金莲道`/`李铭道` приклеили глагол 道 («сказал»).
**Спот-precision по 金瓶梅** (23 терма, разметка моя, один разметчик, претрейн-классика ⇒ предварительно): сущность верна у 20/23 = **0.870**; сущность И тип верны у 19/23 = **0.826**; из 5 алиасов 2 глагольных огрызка.
3. **Собственные дефекты кода, найденные исполнением** (все исправлены, все описаны в шапках скриптов): токенайзер `[A-Za-z]` резал «Jean-François» → `jean-fran`; притяжательные `Diors` шли отдельным кандидатом; строки капслоком давали ложную улику заглавной (`forever/dead/god` в верхушке); заглавные местоимения клеились к именам (`Miss Steele He`, `Anastasia Im`); колонтитул epub («Empire of the Vampire 3 — Empire of the Dawn») дал фантомных кандидатов `dawn` и `Vampire Empire` с рассеянием 125/126 «глав»; контраст кандзи против zh-словаря выродился в ранжирование по частоте.
4. **Мутация пин-копии как причинный тест.** Абляция A3.4 — это не рассуждение о том, какой фильтр виноват, а четыре сборки с по-одному снятым фильтром и четыре замера recall.
### A3.10 Фактура требований к generic-майнеру (хвост №8 D39.37) — не чиню, собираю
| # | Требование | Из какого замера |
|---|---|---|
| G1 | **Алфавит кандидатов — данные пары, не `unicode.Is(unicode.Han)`**. Нужны как минимум: ханьские руны · руны катаканы · капитализованные последовательности с разрешёнными служебными инфиксами | A3.6: en даёт 0, ja теряет весь катакана-класс |
| G2 | **Многословный кандидат с функциональными словами внутри** («Empress of Wolves and Men»). Ханьская n-грамма этого класса не выражает | A3.6 |
| G3 | **Нормализация ключа не должна выводить строку за пределы книги.** trad→simp-фолд на ja даёт `范囲`, которого в книге нет | A3.6, проверено grep |
| G4 | **Порог частоты — на класс, не глобальный.** Техники ja живут на частотах 210, ниже `emitMinFreq=5` | A3.6 |
| G5 | **Разметка исходника — сигнал.** Руби/эмфаза/кавычки-«ёлочки» вокруг термина: канал «одинокой строки» дал 30 кандидатов с precision на глаз ~1.0 | A3.6 |
| G6 | **Контраст-корпус нужен на каждую пару.** Для en кросс-книжный контраст сработал; для ja опоры нет вообще | A3.6 |
| G7 | **Эмиссия должна отделять «что показать владельцу» от «что отдать терминологу».** Сегодня один потолок в 200 обслуживает обе задачи и калечит вторую | A3.4 |
| G8 | **Кластеризация обязана не поглощать родовой титул фамилией.** Признак: поверхность встречается при ≥2 разных фамильных якорях ⇒ она не алиас ни одной из них | A3.5 |
| G9 | **Сложность.** Квадратичность на 7.78 млн символов = 4.6 ч; нужен либо потолок на длину прохода, либо инкрементальный майнинг по частям книги | A3.7 |
| G10 | **Тихая пустая дельта — опасна.** `emitted=0` на не-ханьской книге неотличима от `emitted=0` на чистой; нужен различающий сигнал (сколько кандидатов вообще породил алфавит) | A3.6 |
---
## §A4. Жанровый словарь-драфт — НА ПОДПИСЬ ВЛАДЕЛЬЦУ
### A4.1 Центральный спор: 修炼/修真/修行 — «культивация» или «совершенствование»
**Посылка, которую я проверял:** D39.42 п.1 фиксирует со слов владельца, что общепринятое индустрией — «культивация», а не «совершенствование».
**Что нашли улики.** Рынок расколот, и раскол проходит ровно по линии «лицензионная бумага и академия» vs «фан-площадки».
**Сторона «совершенствование» — первичные источники:**
| Улика | Источник | Дословно |
|---|---|---|
| Эксмо/Комильфо, «Мастер тёмного пути. Том 1», ISBN 978-5-04-232151-1 | eksmo.ru (первичный, издательская аннотация) | «был уничтожен силами **мира совершенствующихся** за свои злодеяния» |
| То же издание, электронная карточка | eksmo.ru | «Чтобы спасти **юных совершенствующихся** от опасности…» |
| Истари Комикс, «Магистр дьявольского культа», 2022, пер. М. Кулишова | аннотация | «кланы **заклинателей**», «**Орден** Гусу Лань» — прямого термина 修炼 нет вообще, выбрана ролевая замена |
| «Духовная культура Китая», т. 2, 2007, ст. Б. Л. Рифтина | synologia.ru | 仙 «**бессмертный**». «В китайской даосской и поздней народной мифологии **разряд святых**» |
| Е. А. Торчинов | сетевое зеркало (статус medium) | 内丹 = «**внутренняя алхимия**»; 丹田 = «**киноварные поля** — центры энергии ци» |
| ru.wikipedia, «Сянься (жанр)» | ru.wikipedia.org | «Главными героями обычно являются «**совершенствующиеся**»» — слов «культивация»/«культиватор» в статье **нет** (проверено адресно мной, не только ресёрчером) |
**Сторона «культивация» — вторичные и площадочные:**
| Улика | Источник | Оговорка верификатора |
|---|---|---|
| тег «культивация», выдача до 137 страниц | tl.rulate.ru | системный тег платформы — улика узуса, принимается |
| тег «Культивация», до 28 страниц | ranobes.com | то же |
| «маги-культиваторы» | «Мир фантастики», 14.07.2021 | **MISQUOTED**: заявленная цитата была грамматически невозможной склейкой; фраза в оригинале относится к китайской НФ, а не к героям сянься. Тезис (журнал употребляет слово) верен, улика ослаблена |
| «Мир Культивации», «Боевые культиваторы» | tl.rulate.ru/collection/12 | **OVER_ATTRIBUTED**: это НЕ редакционное описание площадки, а коллекция пользователя `hentaiman`; сам текст говорит «Данные категории отражают МОЕ понимание» |
| глоссарии ranobe-novels.ru, aoimevelho | фан-глоссарии | см. ниже — это ОДИН источник |
**Ложная сходимость, вскрытая и подтверждённая.** Русские «словари терминов сянься», выглядящие как независимые подтверждения, — ветви одного английского глоссария. Блог `aoimevelho` сам декларирует: «**Частичный перевод этой статьи: immortalmountain.wordpress.com/glossary/wuxia-xianxia-xuanhuan-terms/**». Глоссарий `ranobe-novels.ru` воспроизводит тот же корпус статей — и выдаёт себя machine-translation-артефактом: 灵石 описан как «валюта среди **земледельцев**» (англ. *cultivators* → «земледельцы»). Пост на author.today («культивированием из-за корявого перевода с английского») размножен тем же автором на litnet и pikabu. **Итого: не пять согласных площадок, а один английский глоссарий + один русский блогер.** Это работает в обе стороны: механически подтверждает, что «культивация» действительно пришла калькой через английские фан-переводы, и одновременно обесценивает аргумент «много русских источников согласны».
Аналогично: аннотации на Лабиринте, Читай-городе, book24 и Фантлабе воспроизводят ОДНУ издательскую аннотацию — это один свидетель (издатель), а не четыре. Реально независимых линий узуса — **четыре**: (а) Эксмо/Комильфо → «совершенствующиеся»; (б) Истари → «заклинатели/Орден»; (в) ru-wiki + Фантлаб → «совершенствующиеся»/«адепты»; (г) фан-слой → «культивация».
**Честный вердикт — и главное ограничение, которое его подрезает.**
- **ФАКТ:** в найденном корпусе **аннотаций** лицензионных изданий и в академии «культивация» не зафиксирована ни разу.
- **ФАКТ:** на фан-площадках «культивация/культиватор» — рабочий жанровый ярлык с тысячами тайтлов.
- **⚠ ОГРАНИЧЕНИЕ КЛАССА УЛИКИ (находка критика полноты, снимает половину силы вывода):** вся издательская сторона стоит **ЦЕЛИКОМ НА АННОТАЦИЯХ**. Аннотацию пишет маркетинг, а не переводчик, и она систематически смещена ПРОТИВ жаргона: её задача — быть понятной тому, кто жанра не знает. Это **не «мало улик», а улика не того класса**. Вывод «издательский регистр = совершенствование» на ней **не держится**; держится более слабое: «в маркетинговом тексте издателя жаргон не используется». Тело изданий не читано (§X), а именно оно решает спор.
- **ИНТЕРПРЕТАЦИЯ (моя, помечена):** «рынок устоялся на культивации» верно для площадочного слоя; для издательского слоя вопрос **остаётся открытым до чтения тел изданий**, а не решён в пользу «совершенствования».
- **Довод, которого не было ни у одной стороны и который стоит держать при выборе:** русское «культивация» несёт сельскохозяйственную омонимию (культивация почвы, культиватор как орудие). Для K1/K8 это прямой минус, независимый от узуса; «совершенствование» такой омонимии не имеет, но перегружено общеязыковым значением. Ни один найденный источник этого не обсуждает.
**⟨ВЛАДЕЛЬЦУ⟩ — три варианта, решение ваше. Я не подставляю свой вкус в подписанный контракт.**
| Вариант | Что кладём в `genre-glossary` | Довод за | Довод против |
|---|---|---|---|
| **V1. Издательский регистр** | 修炼/修真/修行 → **совершенствование** | совпадает с бумагой и академией; целимся в «издательский художественный перевод» (формулировка CLAUDE.md) | расходится с ожиданием читателя площадок; ваша исходная посылка ставила «культивацию» |
| **V2. Площадочный регистр** | → **культивация** (как сейчас в untracked-файле) | совпадает с языком аудитории вебновелл; термин компактен и склоняем | не подтверждён ни одним первичным издательским/академическим источником; в бумаге его нет |
| **V3. Расщепление по регистру книги** | поле `genre`/регистр решает: бумажный регистр → «совершенствование», площадочный → «культивация» | улики говорят, что это ДВА узуса, а не один правильный; механизм уже есть — третья колонка `genre` в формате файла | усложняет: требует, чтобы у книги был объявлен регистр, и делает langpack не единственным для пары |
**Моя рекомендация — сначала V0, потом (если придётся выбирать сразу) V3.**
**V0 — не подписывать эту строку сейчас, а сначала закрыть класс улики.** Один заход стоимостью $0 (search-inside по телам двух ISBN + частотный узус по НКРЯ и Google Books Ngram с разбивкой по годам) превращает спор из перецитирования в измерение. Сегодня подписывать предлагается на основании маркетинговых аннотаций — это слабейшая опора из возможных, и сказать это надо ДО подписи, а не после. *(Первая редакция отчёта рекомендовала сразу V3; правка — по находке критика полноты о классе улики.)*
**Если решать без добора — V3, и вот почему это не увиливание:** улики не показывают одного узуса, они показывают два, разделённых по типу издания, а файл `genre-glossary.txt` уже имеет ровно тот механизм, который это выражает (третья колонка — метка, сопоставляемая с полем `genre` книги). V1 сильнее по классу улик среди «однозначных» вариантов, но именно он максимально расходится с исходной посылкой владельца — тем более это решение владельца, не моё.
**Отдельно, чтобы не потерялось:** различение 修炼 / 修真 / 修行 в нашем сиде сегодня стёрто — `修炼` и `修行` имеют один dst «культивация» (найдено машинно, §A2.3 M3). Фан-глоссарий даёт разбор «修真 — совершенствование в истинном; 修行 — совершенствовать поведение и нравственность, отшельничество». Три разных понятия под одним русским словом — это то, что терминолог обязан будет решать, и лучше решить один раз в жанровом словаре.
### A4.2 Транскрипция: система Палладия
Статус (ЭКД, ноябрь 2019): «**Палладица — общепринятая система. Она указывается в китайско-русских словарях, ее используют при переводе официальных и юридических документов**». Нормативная фиксация: «В 1980-х Львом Концевичем для Академии наук СССР была разработана инструкция по передаче средствами русской графики китайских имен собственных». Переводчик китайской литературы Алина Перлова: «В работе строго придерживаюсь палладицы, считаю дурным тоном ее незнание».
Правила, которые ломаются системно (все три уже покрыты нашим langpack — сверено с `palladius.txt` и `palladius-phonotactics.txt`):
- **-ng → -н, -n → -нь** (контринтуитивно; типовая фан-ошибка);
- **zh → чж, не «дж»**: «„Чж“ читается как „дж“… но пишется чж» (Гуанчжоу, не «Гуангжоу»);
- **hui → хуэй** (в топонимах традиционно «хой»), **gui → гуй**, **ü → юй**.
Мелкая фактическая поправка на будущее: ru.wikipedia приписывает создание системы архимандриту Иакинфу (Бичурину), 1839 г., а не самому Палладию Кафарову — популярная атрибуция неточна. Для контракта это ничего не меняет, но в доках лучше не воспроизводить ошибку.
### A4.3 Таблица-драфт НА ПОДПИСЬ
Формат — как у `genre-glossary.txt` (`src<TAB>dst[<TAB>genre]`), но подписывается СОДЕРЖИМОЕ, а не файл: файл лежит в зоне бэкенда, и его правит их сессия.
**Колонка «улика»: `изд.` = лицензионное издание · `акад.` = академический источник · `плщ.` = площадка/фан-глоссарий (зависимая линия, вес низкий) · `UNCLEAR` = улик не нашлось.**
| src | dst (предлагаю) | альтернативы | улика | спорность |
|---|---|---|---|---|
| 修炼 / 修真 / 修行 | **⟨решение V1/V2/V3⟩** | совершенствование · культивация · культивирование · самосовершенствование | изд.+акад. (соверш.) vs плщ. (культив.) | **ВЫСОКАЯ — §A4.1** |
| 仙 | бессмертный | сянь · небожитель | акад. (Рифтин, «Духовная культура Китая») | низкая |
| 气 / 氣 | ци | пневма · эфир · энергия | акад. (веер соответствий: «пневма, эфир… энергия, жизненная сила») | низкая для жанра |
| 丹田 | даньтянь | дантянь · киноварное поле | акад. («киноварные поля»), плщ. («Дантиан») | **средняя: жанр vs академия** |
| 内丹 | внутренняя алхимия | — | акад. (Торчинов) | низкая |
| 金丹 | золотое ядро | золотой эликсир · золотая пилюля | плщ. (жанр) vs акад. («золотой эликсир или золотая пилюля») | **средняя: два регистра** |
| 经脉 | меридианы | каналы | плщ. (одна зависимая линия) | средняя |
| 灵石 | духовные камни | — | плщ. (та же линия, с MT-артефактом) | средняя |
| 内力 | внутренняя энергия | внутренняя сила | плщ. | средняя |
| 修为 | уровень совершенствования | база культивирования | плщ. («База Культивирования» — калька) | **высокая: калька** |
| 天劫 | небесная кара | небесное испытание | плщ. | средняя |
| 妖 | демон / яо | монстр | плщ. | средняя |
| 魔 | дьявол / мо | изверг · демон | плщ. | **высокая: 妖 и 魔 нельзя оба «демон»** |
| 江湖 | цзянху | вольный мир · «Реки-озёра» | акад./энц. | низкая |
| 侠 | ся / рыцарь | герой · странствующий рыцарь | акад./энц. | низкая |
| 走火入魔 | искажение ци | цзоухожумо · отклонение цигун | акад. (CCMD-3: «Цзоухожумо… или отклонение цигун») | **высокая: у жанра своё** |
| 灵气 · 真气 · 筑基 · 元婴 · 剑修 · 心法 · 秘籍 · 功法 · 穴位 · 元神 · 长老 · 掌门 · 内门/外门 · 法宝 · 丹药 · 符箓 · 轻功 · 内功 · 境界 · 飞升 | — | — | **UNCLEAR** | улик не собрано, см. §X |
**Что в словарь НЕ входит (и почему):**
- Книжный канон: `古月`, `方源`, `春秋蝉`, `蛊` — принадлежат 蛊真人, живут в сиде и `CANON-NOTES.md`. Терм книги в общем пар-слое = та самая утечка, которую называет гардрейл общности.
- Чужой фан-канон целиком: массовая выгрузка ranobe-novels/aoimevelho/Fandom — красный класс §A1.3, и правовой (share-alike/БД), и редакционный.
- Строки, где улика — одинокая фан-вики: в таблице они помечены `плщ.` и стоят со «средней/высокой» спорностью именно поэтому, а не для вида.
### A4.4 Замечание по `backend/configs/langpacks/zh-ru/genre-glossary.txt` (чужая зона, не трогал)
Пока я работал, бэкенд-сессия создала этот файл (untracked, 27 строк, куратор объявлен — владелец). Три его строки расходятся с уликами §A4.1A4.3, и это стоит увидеть ДО подписи:
| Строка файла | Что говорят улики |
|---|---|
| `修炼 → культивация` (и 修真, 修行 туда же) | развилка §A4.1; в бумаге и академии — «совершенствование»; плюс три разных понятия сведены в одно слово |
| `筑基 → закладка основания` | улик не найдено; площадочный вариант — «Основание Фонда» (явная калька). Строка **UNCLEAR**, а не общепринятая |
| `修士 → культиватор` | та же развилка, что и 修炼 |
Остальные строки файла (丹田, 灵气, 金丹, 经脉) уликами поддержаны или спорны умеренно. **Это данные для владельца, не правка чужого файла.**
### A4.5 Японская пара: транскрипция, суффиксы, жанровый лексикон
**Транскрипция — Поливанов, но статус слабее, чем у Палладия.** «Система Поливанова сейчас является стандартом де-факто, она используется уже многие годы» — то есть **де-факто, а не ГОСТ**. Нормативная рамка «калька с Хепбёрна = ошибка» сформулирована прямо: такие написания «являются следствием незнания русскоязычными авторами традиций транскрипции японских имён и **не допускаются профессиональными лингвистами**». *Оговорка о ложной сходимости (верификатор):* эта формулировка и полемическая цитата про «суси/суши» — **ОДИН голос**: обе сноски РуВики ведут на Вадима Смоленского (susi.ru, 1998 и 1999). Считать их двумя подтверждениями нельзя. Полемику с другой стороны представляет Николай Караев (otaku.ru): «На сегодня поливановская система — это единственная приличная система транслитерации японского языка»; его же фразу с искажённым написанием имени цитировать дословно НЕЛЬЗЯ — четыре независимых чтения страницы дали 2:2 по написанию, сверки не вышло.
**Позиции ИЗДАТЕЛЬСТВ по транскрипции — UNCLEAR.** Публичных заявлений Истари / Азбуки / Эксмо о системе не найдено. Ближайшее — заявление ПЕРЕВОДЧИКА ранобэ (ник Ushwood; связка «переводчик Истари» держится на одном стороннем источнике, реальное имя **не подтверждено** и в док не ставится): «Вообще-то я при переводе имен с японского обычно руководствуюсь системой Поливанова **если персонажи являются японцами**». То есть в изданной практике Поливанов — правило по умолчанию **с оговоркой по сеттингу**: для псевдоевропейских миров имена передаются на европейский манер. Для нашей книги это прямо релевантно: `異世界魔術師` — исэкай в псевдоевропейском мире, и имена там `ヤード`, `ソフィ`, `マルガレーテ`, `ロベール` (Ярд, Софи, Маргарете, Робер) — **не японские**. Правило «ja ⇒ Поливанов» без оговорки о сеттинге на этой книге даст неверный результат.
**Именные суффиксы — единственная улика издательского уровня.** Представитель Истари Комикс Евгений Кольчугин на прямой вопрос: «Японские именные суффиксы есть, да» (личность отвечающего верификатор сверил). Там же — модель принятия решений по терминам, важная для нас: «Если он [переводчик] говорит, что навык надо локализовать, а тут сделать транслитерацию, **мы соглашаемся**» — то есть издательство отдаёт финальное слово по терминологии переводчику, а не единому корпоративному глоссарию. Академическая картина (Туманов, Вестник ТвГУ. Серия «Филология». 2025. № 2 (85). C. 264270, DOI 10.26456/vtfilol/2025.2.264): единого регламента нет даже на уровне орфографии — дефис против слитного написания; **склонение суффиксов различается по издательствам** (у «Азбуки» не склоняются, у **XL Media** склоняются — название второго издательства верификатор восстановил ручным разбором PDF); «-сама» не склоняется практически всегда. Водораздел — не «как адаптировать», а «сохранять ли вообще»; автор статьи против автоматического сохранения.
**Жанровый лексикон исэкай — улик издательского уровня почти нет.** Найдено (это редакторские решения лицензиата, поэтому ценно):
| src | dst (изданное) | оговорка |
|---|---|---|
| スキル | **навык** | — |
| ダンジョン | **подземелье** | — |
| パーティー | **группа** (не «партия») | — |
| 勇者 | **Герой** | — |
| 転生 | **перерождение** | — |
| 魔法 | **магия** | — |
| ギルド / ランク | **гильдия** / **ранг** | — |
| 魔法使い | **чародей** | единственная твёрдая точка по паре 魔術師/魔法使い |
| 異世界 | «другой мир» **или** «параллельный мир» | **расходится ВНУТРИ одного издательства** ⇒ в словарь не класть |
| 冒険者 | «искатель приключений» **или** «авантюрист» | расходится ⇒ в словарь не класть |
| ステータス · レベル · 魔物/魔獣 · チート · 称号 · 精霊 · 詠唱 · 属性 · 転移 | — | **UNCLEAR**, только фан-площадки |
**魔術師 vs 魔法使い — UNCLEAR, и это прямо про нашу книгу** (`異世界魔術師は魔法を唱えない` — «маг иного мира не произносит заклинаний», где противопоставление 魔術 и 魔法 вынесено в заглавие). Японская сторона различение делает (魔術 = систематизированное/учёное, 魔法 = врождённое/сверхъестественное), но по фан-энциклопедиям, не по словарю. С русской стороны твёрдая точка одна: 魔法使い → «чародей». **Утверждать, что в русских изданиях устоялась пара «маг (魔術師) / волшебник (魔法使い)», нельзя** — это фандомная конвенция вокруг Fate, а не издательская норма. Для книги стенда это означает: различение придётся подписывать владельцу как книжный канон, а не брать из жанрового словаря.
### A4.6 Английская пара: принцип передачи и единство цикла
**Первоисточник жанра — инструкция самого Толкина переводчикам:** «All names not in the following list should be left entirely unchanged in any language used in translation (LT), except that inflexional *s, es* should be rendered according to the grammar of the LT». То есть **презумпция неизменности**, перевод — только для перечисленных «говорящих» имён.
**Русская школа формулирует зеркально:** «смысловые имена собственные переводятся. Это правило», при этом «надо сохранить ту [функцию], которая своей интертекстуальностью, семантикой, экспрессией или информацией обогатит текст». Количественно на корпусе фэнтези-романов: три метода — транслитерирование, транскрибирование, калькирование, причём **транслитерирование доминирует (53,7%)**.
**Рабочий критерий выбора, пригодный для машины, — от Натальи Виленской (переводчик цикла Мартина):** «В работе придерживалась принципа „не англичанить"» и конкретнее: «**Винтерфелл и Хайгарден звучат хорошо, но Блэк Хейвен лучше переделать в Черную Гавань**». Это ровно принцип для `Silversaint` / `Ashdrinker` / `Dawnseeker` из нашей книги: **благозвучный транслит оставляем, неблагозвучный «плоский» англицизм калькируем.** Прямой цитаты про тёмное фэнтези и его неологизмы не найдено — принцип реконструирован, и это помечено как реконструкция.
**Единство терминологии внутри цикла — улики жёсткие, и они прямо подпирают серийный задел владельца.** Виленская: «Без глоссария, конечно, не обойтись, особенно если это дилогия, трилогия и т.д.» И решающее: «Немного позже, получив в перевод „Битву королей", стала придерживаться транскрипции Ю. Соколова, **поскольку первая книга тогда уже вышла**» — **своя система уступает уже опубликованной.** Когда политика ломается, видно у переводчика Аберкромби: «основная часть проблем всех официальных переводов Аберкромби заключается в том, что разные книжки в серии переводят разные переводчики», следствие — «множество повторяющихся фраз, афоризмов, диалогов, шуток над фразами из прошлых книжек которые при переводе разными людьми бесследно теряются».
**Что из этого следует для контракта (моё чтение, не улика):** «round-trip подписанного глоссария как сид следующей книги» (задел владельца 26.07) — это машинная реализация ровно того правила, которое издательская практика формулирует словами «уступать уже вышедшему тому». А класс дефекта «разные переводчики — потерянные переклички» — это то, чего наш банк по построению не допускает, и это стоит записать как заявленное преимущество, а не как побочный эффект.
---
## §A5. Пре-регистрация фазы B
Всё ниже фиксируется ДО трат и не переписывается после. Критерий ПРОВАЛА назван заранее.
### A5.1 Вопрос фазы B
Один: **какой промт терминолога даёт лучшее консолидированное dst на смердженном банке, и меняет ли веб-справка результат настолько, чтобы её строить.**
### A5.2 Арматура промтов (≥3 осмысленно разных)
| Арм | Что в промте | Что проверяет |
|---|---|---|
| **P1 «голый контекст»** | src + N KWIC-контекстов + варианты черновиков. Ни жанрового словаря, ни Палладия | базовая линия: сколько даёт один контекст |
| **P2 «контекст + пар-данные»** | P1 + жанровый словарь §A4 + правила Палладия + требование склоняемой леммы | вклад langpack-данных (гипотеза: главный рычаг) |
| **P3 «контекст + пар-данные + рубрика»** | P2 + критерии §A2 прямо в промте + требование самооценки по ним | улучшает ли явная рубрика выход модели |
| **P4 «веб-справка»** (ответ на D39.42 п.2) | P2 + предварительно добытая справка по терму (офлайн-снапшот, НЕ живой фетч в петле) | нужен ли веб-фетч в v2 роли |
| **P0 плацебо — СЛУЧАЙНАЯ терминология** | P2, но жанровый словарь подменён случайными парами того же вида | контроль по рецепту WMT: если P2 ≈ P0, прирост даёт сам факт наличия подсказки, а не её правильность |
| **P0b контроль контекстов** | src без KWIC-контекстов вообще | если P0b ≈ P1, контексты не работают и конструкция входа неверна |
**Почему офлайн-снапшот вместо живого фетча:** живой фетч в петле недетерминирован (страница меняется), а инвариант детерминизма №8 держит весь наш golden. Снапшот отвечает на вопрос «улучшает ли справка» и не ломает воспроизводимость. Если ответ «улучшает» — вопрос «как доставлять» решается отдельно.
### A5.3 Выборки
| Выборка | Книга | Состав | Зачем |
|---|---|---|---|
| S1 | 蛊真人 | 30 термов: 15 подписанных владельцем (эталон есть) + 15 намайненных без dst | единственная выборка с золотым стандартом |
| S2 | 金瓶梅 | 15 термов из эмитированных | второй zh, другой жанр; претрейн-оговорка |
| S3 | 異世界魔術師 | 15 термов: 5 катакана + 5 «одинокая строка» + 5 кандзи | ja; проверяет класс, которого движок не видит |
| S4 | Empire of the Dawn | 15 термов: 8 однословных + 7 многословных | en; проверяет класс G2 |
| **S5 ловушка** | 蛊真人 | 5 термов, где подписанный dst НАРУШАЕТ буквальность ради канона (时辰 = 2 часа, generic-«гу» ср. род, 资质 = «талант» — D39.21) | ловит модель, «улучшающую» подписанный канон |
Итого 80 термов × 6 армов (P0, P0b, P1P4) = 480 консолидаций.
**⚠ Операционный дефект дизайна, вскрытый критиком полноты ДО трат — и я его чиню здесь, а не после.** Выборки S3 (ja) и S4 (en) — это 30 из 80 термов, **37% замера**, и по ним предполагалось оценивать критерий K2 «жанровая конвенция». Но §A4 по ja и en почти пуст: для японского исэкай-лексикона улик издательского уровня нашлось девять строк из двадцати, для английского — общий принцип без словаря. **Оценивать соответствие конвенции, которой у нас нет, значит купить бессмысленный вердикт.** Поэтому в пре-регистрацию вносится правило: **на S3 и S4 критерий K2 НЕ оценивается вовсе** (помечается `н/д`, как «н/д» в пакете-6), а не оценивается пустым. Оцениваются K1, K5, K6, K7, K9, K11 и машинные. Если владелец хочет K2 и на ja/en — сначала нужен добор улик (Z4), и это отдельный $0-заход перед фазой B.
**Второе изменение по той же находке: арм «живой профессиональный переводчик».** Критик указал на дыру, которую все шесть срезов пропустили: **у трёх из пяти книг стенда почти наверняка есть изданный русский перевод** — 金瓶梅 в переводе В. С. Манухина («Цветы сливы в золотой вазе», с научным аппаратом и указателем реалий, то есть с готовым академическим глоссарием), Fifty Shades (Эксмо) и Empire of the Vampire (русское издание Кристоффа, где Silversaint/Ashdrinker уже переданы — плюс это ровно кейс переноса терминологии между томами серии, который отчёт объявил ненайденным). **Это готовый человеческий эталон, которого у нас, как я написал в первой редакции, «нет ни на одной книге, кроме сида».** Проверка существования — один запрос, $0. Пре-регистрирую: **до фазы B проверить наличие; где перевод есть — добавить арм `HUMAN` в слепую выборку.** Это и закрывает первый вопрос, который задаст владелец («а как это перевёл живой переводчик?»), и делает BWS-оценку осмысленной (BWS силён именно на различении «человек vs машина»).
**Оговорка о мощности выборки, названная заранее:** MQM-практика считает, что для низкой дисперсии нужно >200 предложений, а <1517 вообще не аналитическая оценка, а статконтроль. Наши 80 термов это между. Поэтому фаза B заявляется как **отбор промта, а не как аттестация качества**: она вправе сказать «арм X устойчиво лучше арма Y» и НЕ вправе сказать «терминолог даёт качество Z».
### A5.4 Слепая оценка
По протоколу §A2.4, то есть **Best-Worst Scaling по каждому судейскому критерию отдельно**, а не покритериальные абсолютные баллы. Машинный слой (K3/K4/K8/K10/K12 — $0, матчинг ЛЕММАТИЗИРОВАННЫЙ по рецепту WMT) идёт первым; к судье/человеку попадает только то, что его прошло. На выборках S3 (ja) и S4 (en) критерий K2 помечается `н/д` и не оценивается (см. оговорку в A5.3). Судейские вызовы — по риг-стандарту D39.7, ≤2 семейства, судья не судит своё семейство.
### A5.5 Смета
Считана от фактических цен стека (стек и цены — `docs/experiments/08-cost-model-v2.md`, `00-provider-quirks.md`), объём — от измеренного размера KWIC-контекстов.
| Статья | Расчёт | Оценка |
|---|---|---|
| Консолидация: 80 термов × 6 армов, ~8 контекстов × ~120 симв. + инструкция ≈ 2.5k вх. / 300 вых. токенов на терм | 480 вызовов, дешёвая модель | **$0.350.85** |
| Слепая оценка Best-Worst Scaling: только термы, прошедшие машинный слой (ожидаю ~60%), 3 повтора со свапом | ~700 судейских вызовов | **$0.601.30** |
| Резерв на брак и повтор (норма проекта: брак отчитывается, не скрывается) | +30% | **$0.300.60** |
| **Итого** | | **$1.202.60**, потолок промта ≤$3 держится |
**Смета масштабирования, отдельно и честно.** Если терминолог поедет на полном банке (13k кандидатов варианта C), а не на выборке: 13 246 × ~2.5k входных токенов ≈ 33 млн входных токенов на книгу. Это **не «центы/книга»**, как записано в D39.42 п.1, а единицы долларов даже на самой дешёвой модели, и это до батчинга. Пре-регистрирую это как измерение фазы B: **реальная цена терминолога на полном банке — вход в решение, строить ли ось «что показать владельцу» отдельно от «что отдать терминологу» (требование G7).**
### A5.6 Критерий ПРОВАЛА (назван до трат)
Фаза B объявляется провалившейся, если выполнено любое:
- **F1.** P0b (без контекстов) не отличим от P1 ⇒ контексты не работают, конструкция входа неверна.
- **F1b.** **P0 (СЛУЧАЙНАЯ терминология) не отличим от P2** ⇒ прирост даёт факт подсказки, а не её правильность; весь замер терминологии — самообман (воспроизведён эффект WMT23), и жанровый словарь свою цену не оправдывает.
- **F2.** Ни один арм не бьёт подписанный владельцем dst на S1 хотя бы вничью по K1 ⇒ роль не готова к авто-режиму.
- **F3.** Любой арм проваливает ловушку S5 (переписывает канон) чаще, чем в 1 случае из 5 ⇒ авто-режим опасен без подписи независимо от качества.
- **F4.** Krippendorff α по K1 ниже 0.6, либо leave-one-out по оценщику меняет победителя ⇒ мерить нечем, результат не выдаём.
---
## §S. Свои гипотезы (мандат свободы) — пре-регистрация
Отдельной секцией, не смешивая с обязательным. Каждая: гипотеза → как меряю → что опровергнет.
| # | Гипотеза | Как меряю | Что опровергнет |
|---|---|---|---|
| **H1** | Главный рычаг качества терминолога — не промт, а **пар-данные** (жанровый словарь + Палладий). P2 обгонит P1 сильнее, чем P3 обгонит P2 | разность средних по K1+K2 между армами на S1S4 | если P3P2 ≥ P2P1, рычаг — рубрика в промте, а не данные |
| **H2** | **Орфографическая улика бьёт частотную** на языках, где она есть. Кандидаты канала «одинокая строка» (ja) получат на слепой оценке балл выше, чем кандидаты канала кандзи той же частоты | сравнение K1 по каналам на S3 при контроле частоты | если разницы нет, разметка исходника — не сигнал, и G5 отпадает |
| **H3** | **Терм, встречающийся в ≥2 книгах жанра, — жанровый**; встречающийся в одной — книжный. Операциональный критерий «жанровости» | пересечение кандидатов 蛊真人 ∩ 金瓶梅 и en∩en; сверка с ручной разметкой §A3.8 | если пересечение наполнено служебной лексикой, а не жанровыми терминами, критерий не работает |
| **H4** | **Веб-справка помогает избирательно**: на классах «жанровое понятие» и «титул» даст прирост, на классе «личное имя» — нет (имя выдумано автором, справки о нём не существует) | P4 vs P2 в разрезе по классам §A3.8 | равномерный прирост или равномерный ноль ⇒ гипотеза о избирательности неверна |
| **H5** | **Разброс вариантов черновика предсказывает трудность терма** — там, где черновики дали 3+ разных dst, слепая оценка даст низкий K1 у всех армов | корреляция «число вариантов банкноты» × «средний K1» | отсутствие корреляции ⇒ разброс нельзя использовать как дешёвый приоритет для человека |
H1 и H4 адресованы прямо решениям D39.42 (пар-данные в промте; веб-фетч в v2). H2, H3, H5 — мои.
---
## §X. Чего эта фаза НЕ покрывает
Раздел собран не только мной: по отчёту прошёл отдельный **критик полноты**, которому вменялось искать дырки. Его находки приняты целиком, часть уже вшита правками в §A1.0, §A2.0, §A4.1, §A5.3; остальное — здесь.
**Главная дыра, названная критиком (я её пропустил в шести срезах из шести):**
0. **Изданные русские переводы КНИГ СТЕНДА не искались вообще.** Почти наверняка существуют минимум для трёх из пяти (Манухин для 金瓶梅 с указателем реалий; Эксмо для Fifty Shades; русский Кристофф). Цена пропуска двойная: у фазы B нет человеческого эталона там, где он мог быть, и §A4 остался без единственного источника класса «изданный перевод» **с телом текста**, а не с аннотацией. Исправлено в §A5.3 как обязательная предпроверка.
**Модальности и углы, не задействованные ни разу** (каждый — конкретный дешёвый заход, не абстрактное «можно было лучше»):
- **Тело изданий через фрагмент**: search-inside Google Books по ISBN, ЛитРес-фрагмент, фото разворотов в отзывах маркетплейсов. Именно эта модальность решает спор §A4.1, а мы работали по аннотациям.
- **Частотный узус вместо цитатного**: НКРЯ и Google Books Ngram (rus) по «культивация»/«культиватор»/«совершенствующийся» с разбивкой по годам. Спор V1/V2/V3 решается измерением с трендом, а вёлся перечислением цитат.
- **Архив/зеркало**: web.archive.org и archive.today не пробованы **ни разу**, хотя минимум шесть UNCLEAR имеют причиной 403/Cloudflare.
- **Локальный парсинг вместо веб-фетча**: у сессии есть pdftotext и OCR, а ГУГК-1983 и Концевич-2002 объявлены недоступными «потому что PDF-скан»; у БКРС есть скачиваемый дамп базы, а печатный Ошанин лежит на archive.org.
- **Языки запроса**: японский — ноль запросов (пропущен отраслевой стандарт оценки качества JTF, независимый от MQM/ISO-куста); китайский — эпизодически, при том что различение 修真/修炼/修行, объявленное обязательным, ищется в 知乎/百度百科 одним запросом; польский/чешский — ноль, хотя это прямой аналог нашей задачи (глоссарий во флективный язык).
- **Настоящие конкуренты по продукту не смотрелись.** Срез «конкуренты» ушёл к MT-вендорам (Microsoft/Amazon/DeepL/Google), а владелец просил посмотреть у конкурентов. **GalTransl, SakuraLLM, AiNiee — открытые репозитории LLM-переводчиков новелл с глоссарием, где промты и схемы лежат в исходниках.** Это самая близкая к нам практика, и она не открыта.
- **Реальные термбазы вместо документации о них**: Microsoft Terminology Collection (ru, TBX) и IATE-экспорт показали бы, какие поля ФАКТИЧЕСКИ заполняют.
- **Класс отказа как терминологический дефект**: 18+-терминология (`Dominant`/`Submissive` из книги стенда) и поведение судьи на ней не исследованы — при том, что это боевой риск проекта.
**Сорвалось / ограничено:**
1. **Бюджет веб-поиска сессии исчерпан (200/200)**, и израсходован неэффективно: шесть углов независимо переоткрывали одни и те же вики-страницы, а на 20 UNCLEAR-строк словаря не осталось ничего. **Процессный вывод в копилку: следующий заход начинать с резервирования квоты под конкретную таблицу, а не с широкого обзора.** Добор улик по 20 терминам со статусом UNCLEAR в §A4.3 и по японскому жанровому лексикону (`ステータス`, `レベル`, `魔物`, `チート`, `称号`, `精霊`, `詠唱`, `属性`, `転移`) в этой сессии невозможен. Часть верификаторов работала уже в условиях исчерпанного поиска (только WebFetch) и без доступа к web.archive.org, что снижает их способность искать альтернативные зеркала. Один пункт (написание искажённого имени у Караева) остался неразрешённым: четыре чтения страницы дали 2:2.
3. **БКРС не читается веб-фетчем** (страница рендерится JS) — «что говорит словарь» осталось UNCLEAR по всем терминам. Это самый весомый пробел §A4: словарная улика сильнее площадочной, и её у нас нет.
4. **Тела лицензионных изданий не читаны** — проверены только аннотации. Как Истари/Эксмо/АСТ передают 修炼, 金丹, 丹田, 元婴, 筑基 внутри текста, неизвестно. Есть ли в томах печатный глоссарий переводчика — не найдено.
5. **TDM-исключения (EU DSM ст. 3/4, японская ст. 30-4)** первоисточником не подтверждены.
6. **Издавался ли Цзинь Юн на русском лицензионно** — не найдено ничего.
**Не делалось сознательно:**
7. **Профиль квадратичности не снят** — источники сложности названы по коду и НЕ доказаны исполнением (§A3.7).
8. **Precision майнинга измерен спот-чеком, одним разметчиком, без слепоты.** 0.870 по 金瓶梅 и ≈0.15 по абляции C — это ориентиры, а не метрики уровня пакета-6.
9. **Детерминизм проверен ре-раном, но выборочно:** `gu10` и `jpm` пере-прогнаны и дали **побайтно идентичный** выход (`cmp` чист). `gu25`/`gu50`/`gu100` и абляции B/C/D прогонялись по одному разу — на них детерминизм принят по построению и по двум проверенным срезам, а не измерен.
10. **Роль терминолога в коде не проектировалась** — по прямому запрету промта. Требования G1G10 — фактура, не дизайн.
11. **Претрейн-оговорка в силе:** всё, снятое на 金瓶梅 и Fifty Shades, предварительно.
12. **Покрытие словаря §A4.3 — около 40%.** Из ~34 строк 20 стоят UNCLEAR. Подписывать по факту есть что, но меньше половины; остальное — предмет добора Z4.
13. **Правило провенанса §A1.3 сегодня нечем заполнить до конца:** ToS, `robots.txt` и TDM-резервирование ключевых источников (БКРС, Fandom, Викисловарь) не проверялись. Рамка есть, поле `licence_class` для части источников заполнить нечем.
14. **«Как ведут глоссарий между томами серии» — прямых улик так и нет ни за, ни против**, при том что серийный шаринг банка — свежая архитектурная ставка (D39.42). Ближайшее найденное — что непрерывность обеспечивается сверщиком и редактором, а документ не упомянут.
15. **Деньги названы в токенах, не в долларах.** 33 млн входных токенов на полный банк (§A5.5) не переведены в цену по актуальному прайсу, и нет baseline «сколько теряем, если терминолога НЕ делать».
---
## §Z. Что на владельце
| # | Решение | Раздел |
|---|---|---|
| **Z1** | **Ратифицировать рубрику §A2** и ответить на Q1Q4 (веса · K2 против K1 · порог «хорошо» · сокращённая форма и Р3 из D39.39) | §A2.5 |
| **Z2** | По 修炼 — выбрать **V0 / V1 / V2 / V3**. Моя честная рекомендация после правки критиком: **V0 (не подписывать сейчас, сначала закрыть класс улики)**, потому что издательская сторона спора стоит на маркетинговых аннотациях, а не на телах книг. От решения зависят три строки уже созданного `genre-glossary.txt` | §A4.1, §A4.4 |
| **Z3** | Санкционировать **смету фазы B $1.202.60** и пре-регистрацию A5 (включая критерий провала F1F4, снятие K2 с ja/en-выборок и арм `HUMAN`) | §A5 |
| **Z4** | Решить, **делать ли добор улик отдельным $0-заходом ПЕРЕД фазой B.** Пять адресных заходов с ожидаемым выхлопом: (1) search-inside по телам двух ISBN → решает Z2 и до 20 UNCLEAR-строк; (2) изданные русские переводы книг стенда → человеческий эталон на 3 из 5 книг; (3) НКРЯ + Google Books Ngram → превращает Z2 из спора в измерение; (4) архивы/локальный парсинг → снимает большинство не-CONFIRMED; (5) GalTransl/SakuraLLM/AiNiee + JTF-гайдлайн → настоящие конкуренты и слой, независимый от MQM/ISO | §X |
| **Z5** | Оркестратору на заметку: **фактура G1G10 и цифры §A3 — прямой вход в фазу 2 пака-20 и в будущий пак generic-майнера**; отдельно — дефект кластеризации §A3.5, стоивший подписанного терма | §A3 |
**Заявление = команда.** Приёмка воспроизводима: `build.sh <dir> 996bade`, команды §0.2, ожидаемый результат — те же цифры (майнер детерминирован; мои скрипты — тоже, случайная выборка засолена фиксированным сидом 20260726).

22
eval/pkg7/README.md Normal file
View file

@ -0,0 +1,22 @@
# `eval/pkg7/` — терминологическая ресёрч-программа (полигон, пакет-7)
Инструменты фазы A. Отчёт с цифрами и вердиктами — `docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md`.
Всё здесь **$0**: ни одного вызова LLM-провайдера, ни одного чтения ключей.
| Скрипт | Что делает | Заметки |
|---|---|---|
| `split_book.py` | книга → JSONL-субстрат `{chapter, chunk_idx, source}` | правила глав — ДАННЫЕ (`RULES`), добавить книгу = добавить строку; epub парсится stdlib (bs4/ebooklib в venv нет) и чистится от колонтитулов |
| `minerharness/` | вызов БОЕВОГО `internal/miner` из модуля ВНЕ репо | `build.sh <work-dir> [commit]`; `replace` смотрит в `git archive`-копию коммита, а НЕ в рабочее дерево |
| `mine_nonhan.py` | кандидаты-термы для en/ja тремя раздельными каналами | ORTHO · DISP · CONTRAST; кандзи сознательно без контраста (см. шапку) |
| `kwic.py` | KWIC-контексты вхождения | разметка по тексту, а не по памяти; `--stats` даёт объём контекстов для сметы |
| `seed_recall.py` | recall WHICH-канала против подписанного сида | майнер обязан гоняться с ПУСТЫМ сидом, иначе метрика вырождается |
| `rubric_probe.py` | измеримость критериев рубрики на подписанном сиде | M1 морфология (pymorphy3) · M2 транскрипция · M3 коллизии банка |
## Правила, выученные на своих ошибках в этом пакете
1. **Пин обязателен.** Харнесс `replace`-ит копию коммита, не рабочее дерево: параллельная бэкенд-сессия правит `backend/` под руками, и прогон стал бы невоспроизводимым. Все цифры отчёта — на `996bade`.
2. **Ссылки `файл:строка` — по коммиту пина.** В рабочем дереве те же якоря уже уехали; первая редакция отчёта содержала номера строк рабочего дерева.
3. **Порог частоты и алфавит кандидатов — свойство ЯЗЫКА, не движка.** Японские названия техник живут на частотах 210 и в катакане/руби; ханьский n-граммный канал их не видит вовсе.
4. **Правило вершины словосочетания зависит от типа терма** (у имени вершина последняя, у нарицательного — первая). Морфо-чекер без этого правила меряет не то: первая версия `rubric_probe.py` склоняла «Вина» в «Монах Цветочного Вина».
5. **Контраст без опоры вырождается в частоту.** Ранжирование кандзи против словаря другого языка даёт log частоты, а не над-представленность; канал понижен до инвентаря.
6. **Отклонение методики — замерять, а не декларировать.** Нарезка здесь не боевая; свип целевого размера чанка (800/2000/6000) дал Жаккар 1.000 — только после этого цифры можно было предъявлять.

63
eval/pkg7/kwic.py Normal file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Полигон, пакет-7: KWIC-контексты вхождений термина по субстрату чанков.
Нужен дважды. (1) Разметка кандидатов не по памяти, а по тексту: любое утверждение «это имя
персонажа / это жанровый термин» обязано опираться на вхождения, иначе это угадывание. (2) Прототип
того, что роль ТЕРМИНОЛОГ получает на вход по D39.42 п.1 («KWIC-контексты вхождений из исходника»),
здесь видно, сколько это стоит по символам, и это цифра для сметы фазы B.
$0, stdlib. Детерминирован: вхождения идут в порядке (chapter, chunk_idx, offset).
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
def load(path: Path) -> list[dict]:
return [json.loads(l) for l in path.open(encoding="utf-8") if l.strip()]
def kwic(rows: list[dict], needle: str, width: int, limit: int) -> list[tuple[int, int, str]]:
out = []
for r in rows:
src, pos = r["source"], 0
while True:
i = src.find(needle, pos)
if i < 0:
break
a, b = max(0, i - width), min(len(src), i + len(needle) + width)
frag = src[a:b].replace("\n", "")
out.append((r["chapter"], r["chunk_idx"], frag))
pos = i + len(needle)
if limit and len(out) >= limit:
return out
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--chunks", required=True, type=Path)
ap.add_argument("--term", action="append", required=True)
ap.add_argument("--width", type=int, default=40)
ap.add_argument("--limit", type=int, default=6)
ap.add_argument("--stats", action="store_true", help="только объём контекстов (для сметы)")
a = ap.parse_args()
rows = load(a.chunks)
for t in a.term:
hits = kwic(rows, t, a.width, 0 if a.stats else a.limit)
if a.stats:
chars = sum(len(f) for _, _, f in hits)
print(f"{t}\tвхождений={len(hits)}\tсимволов_контекста={chars}")
continue
print(f"=== {t} ({len(hits)} показано, лимит {a.limit}) ===")
for ch, ci, frag in hits:
print(f" [гл.{ch}/{ci}] …{frag}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

285
eval/pkg7/mine_nonhan.py Normal file
View file

@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Полигон, пакет-7 (A3): майнинг кандидатов-терминов для НЕ-ханьских книг (en, ja).
Зачем отдельный скрипт. Боевой `internal/miner` строит кандидатов ТОЛЬКО из ханьских рун
(`miner_substrate.go:44-63`, `isMinerHan`), поэтому на английском он по построению отдаёт пустую
дельту, а на японском видит лишь кандзи катакана, где у исэкая живёт большая часть жанровых
терминов, ему невидима. Этот скрипт не замена движку, а СБОР ФАКТУРЫ ТРЕБОВАНИЙ к будущему
generic-майнеру (хвост 8 D39.37): какие каналы нужны, что каждый ловит, чем платим.
Три НЕЗАВИСИМЫХ канала (считаются и отчитываются раздельно их согласие не подмешивается в один
балл, чтобы не собрать ложную сходимость):
ORTHO орфографическая улика языка: en заглавная НЕ в начале предложения; ja руны катаканы,
n-граммы кандзи, «одинокая короткая строка» (уплощённая рубя/эмфаза дампа syosetu).
DISP рассеяние: в скольких главах встречается (термин книги живёт по всей книге, реплика нет).
CONTRAST над-представленность против ОПОРНОГО корпуса. en опора берётся из ДРУГОЙ английской
книги стенда (кросс-книжный контраст: дешёвая и честная замена «общего корпуса»);
ja/кандзи опоры НЕТ. Первая версия скрипта пробовала подставить сюда jieba-словарь
(общий zh) и получила вырожденное ранжирование: для слова, которого в опоре нет,
лапласова вероятность постоянна, поэтому «над-представленность» вырождается в
log2(частоты) и наверх всплывают 彼女// обычная японская лексика. Канал кандзи
поэтому отдан БОЕВОМУ майнеру (он считает ожидание по независимости символов,
`miner_substrate.go:197 charIndepRel`, а это работает и на японском), а здесь остаётся
ИНВЕНТАРЁМ класса частоты без ранга, чтобы видеть объём, а не выдавать порядок за
результат. Катаканы в zh-опоре нет вообще, у неё канала CONTRAST не существует.
$0: ни одного сетевого вызова, ни одной модели, ни одной внешней зависимости (только stdlib).
"""
from __future__ import annotations
import argparse
import json
import math
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
# --- алфавиты ----------------------------------------------------------------------------------
KATAKANA = re.compile(r"[ァ-ヺヽヾㇰ-ㇿ][ァ-ヺーヽヾㇰ-ㇿ・]*")
HAN_RUN = re.compile(r"[㐀-䶿一-鿿豈-﫿]+")
HIRAGANA = re.compile(r"[ぁ-ゟ]")
# Латиница с диакритикой: François, Ilsæ, Ashdrinker — обрезать по [A-Za-z] значит плодить огрызки
# («jean-fran»), что первый прогон и сделал.
EN_TOKEN = re.compile(r"[A-Za-zÀ-ÖØ-öø-ÿÆæŒœ][A-Za-zÀ-ÖØ-öø-ÿÆæŒœ'\-]*")
POSSESSIVE = re.compile(r"[']s$")
EN_INFIX = {"of", "the", "de", "du", "la", "le", "von", "van", "des", "di", "da", "and", "à", "san"}
# Английские местоимения и стяжения пишутся с заглавной по грамматике, а не потому, что это имя.
# Первый прогон без этого списка склеил «Miss Steele He», «Anastasia Im», «Kate I» — заглавное
# местоимение приклеилось к настоящему имени и породило фантомный многословный «термин».
EN_PRONOUN = {
"i", "he", "she", "it", "we", "they", "you", "him", "her", "his", "hers", "them", "their",
"me", "my", "mine", "our", "ours", "your", "yours", "its",
"i'm", "im", "i've", "ive", "i'd", "id", "i'll", "ill",
}
EN_STOP = {
"i", "a", "an", "and", "but", "or", "so", "then", "there", "this", "that", "these", "those",
"he", "she", "it", "we", "you", "they", "his", "her", "my", "your", "our", "their", "what",
"when", "where", "why", "how", "who", "if", "as", "at", "by", "for", "from", "in", "into",
"of", "on", "to", "with", "yes", "no", "oh", "ok", "okay", "well", "not", "do", "did", "does",
"is", "are", "was", "were", "be", "been", "am", "have", "has", "had", "will", "would", "can",
"could", "should", "shall", "may", "might", "must", "one", "two", "all", "some", "any",
"very", "just", "now", "here", "after", "before", "again", "still", "even", "only", "also",
"like", "up", "down", "out", "over", "under", "about", "because", "while", "though",
"although", "however", "maybe", "mr", "mrs", "ms", "dr", "sir", "madam", "s", "t", "est",
} | EN_INFIX | EN_PRONOUN
def load_chunks(path: Path) -> list[dict]:
rows = []
with path.open(encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def load_zh_contrast(path: Path) -> Counter:
"""jieba `word freq POS` → Counter. Тот же артефакт, что читает боевой майнер."""
c: Counter = Counter()
with path.open(encoding="utf-8", errors="replace") as f:
for line in f:
parts = line.split()
if len(parts) >= 2 and parts[1].isdigit():
c[parts[0]] = max(c[parts[0]], int(parts[1]))
return c
# --- канал ORTHO: английский --------------------------------------------------------------------
def en_candidates(rows: list[dict]) -> tuple[Counter, dict[str, set[int]], Counter, Counter]:
"""(cap_mid, главы, multiword-freq в том же счётчике, tok_total).
Улика заглавная НЕ в начале предложения/строки. Строки, набранные ЦЕЛИКОМ прописными
(колонтитул, шмуцтитул, эпиграф капслоком), из улики исключены: там заглавная ничего не значит,
и первый прогон именно на них поймал «forever/dead/god» в верхушку списка.
"""
cap_mid: Counter = Counter()
tok_total: Counter = Counter()
chapters: dict[str, set[int]] = defaultdict(set)
multi: Counter = Counter()
multi_ch: dict[str, set[int]] = defaultdict(set)
for r in rows:
for raw_line in r["source"].split("\n"):
letters = [c for c in raw_line if c.isalpha()]
all_caps_line = bool(letters) and all(c.isupper() for c in letters)
for sent in re.split(r"(?<=[.!?])\s+", raw_line):
toks = [POSSESSIVE.sub("", m.group(0)) for m in EN_TOKEN.finditer(sent)]
toks = [t for t in toks if t]
if not toks:
continue
for i, t in enumerate(toks):
low = t.lower()
tok_total[low] += 1
if i > 0 and t[0].isupper() and not all_caps_line:
cap_mid[low] += 1
chapters[low].add(r["chapter"])
if all_caps_line:
continue
i = 0
while i < len(toks):
if i > 0 and toks[i][0].isupper() and toks[i].lower() not in EN_PRONOUN:
j, last_cap = i, i
while j + 1 < len(toks):
nxt = toks[j + 1]
if nxt[0].isupper() and nxt.lower() not in EN_PRONOUN:
j += 1
last_cap = j
elif (nxt.lower() in EN_INFIX and j + 2 <= len(toks) - 1
and toks[j + 2][0].isupper()):
j += 1
else:
break
if last_cap > i:
phrase = " ".join(toks[i:last_cap + 1])
multi[phrase] += 1
multi_ch[phrase].add(r["chapter"])
i = last_cap + 1
else:
i += 1
for p, c in multi.items():
cap_mid[p] = c
tok_total[p] = c
chapters[p] = multi_ch[p]
return cap_mid, chapters, cap_mid, tok_total
# --- канал ORTHO: японский ----------------------------------------------------------------------
def ja_candidates(rows: list[dict], ngram_max: int = 6):
freq: dict[str, Counter] = {"katakana": Counter(), "kanji": Counter(), "loneline": Counter()}
chaps: dict[str, dict[str, set[int]]] = {k: defaultdict(set) for k in freq}
for r in rows:
src = r["source"]
for m in KATAKANA.finditer(src):
w = m.group(0).strip("・ー")
if len(w) >= 2:
freq["katakana"][w] += 1
chaps["katakana"][w].add(r["chapter"])
for m in HAN_RUN.finditer(src):
run = m.group(0)
L = len(run)
for n in range(1, ngram_max + 1):
for i in range(0, L - n + 1):
g = run[i:i + n]
freq["kanji"][g] += 1
chaps["kanji"][g].add(r["chapter"])
for line in src.split("\n"):
s = line.strip()
if 1 <= len(s) <= 8 and not HIRAGANA.search(s) and HAN_RUN.fullmatch(s):
freq["loneline"][s] += 1
chaps["loneline"][s].add(r["chapter"])
return freq, chaps
# --- канал CONTRAST ------------------------------------------------------------------------------
def contrast_scores(book: Counter, ref: Counter) -> dict[str, float]:
"""log2 над-представленности с лапласовым сглаживанием; опора — частоты опорного корпуса."""
nb, nr = sum(book.values()) or 1, sum(ref.values()) or 1
denom = nr + len(ref) + 1
out = {}
for w, f in book.items():
pb = f / nb
pr = (ref.get(w, 0) + 1) / denom
out[w] = math.log2(pb / pr)
return out
def emit(rows_out: list[dict], out: Path, note: str) -> None:
out.parent.mkdir(parents=True, exist_ok=True)
with out.open("w", encoding="utf-8") as f:
for r in rows_out:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f"{out}: {len(rows_out)} кандидатов [{note}]", file=sys.stderr)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--in", dest="inp", required=True, type=Path)
ap.add_argument("--lang", required=True, choices=["en", "ja"])
ap.add_argument("--ref", type=Path, help="JSONL опорной книги (кросс-книжный контраст, en)")
ap.add_argument("--zh-contrast", type=Path, help="jieba dict как приблизительная опора для кандзи (ja)")
ap.add_argument("--min-freq", type=int, default=5)
ap.add_argument("--min-chapters", type=int, default=2)
ap.add_argument("--top", type=int, default=200, help="ТОП НА КАНАЛ, не суммарно")
ap.add_argument("--out", required=True, type=Path)
a = ap.parse_args()
rows = load_chunks(a.inp)
result: list[dict] = []
if a.lang == "en":
cap_mid, chapters, _, tok_total = en_candidates(rows)
ref_cap: Counter = Counter()
if a.ref:
ref_cap, _, _, _ = en_candidates(load_chunks(a.ref))
cs = contrast_scores(cap_mid, ref_cap) if ref_cap else {}
for w, f in cap_mid.items():
if f < a.min_freq or w in EN_STOP:
continue
nch = len(chapters[w])
capratio = f / max(tok_total[w], 1)
if nch < a.min_chapters and f < a.min_freq * 3:
continue
if capratio < 0.5:
continue
ch_list = ["ORTHO"]
if nch >= a.min_chapters:
ch_list.append("DISP")
if cs.get(w, 0) >= 3:
ch_list.append("CONTRAST")
result.append({
"src": w, "freq": f, "chapters": nch, "cap_ratio": round(capratio, 3),
"contrast_log2": round(cs[w], 2) if w in cs else None,
"channels": ch_list, "words": len(w.split()), "channel": "en-proper",
})
result.sort(key=lambda r: (-(r["contrast_log2"] or 0), -r["freq"], r["src"]))
result = result[: a.top] if a.top else result
else:
freq, chaps = ja_candidates(rows)
# Канал кандзи здесь СОЗНАТЕЛЬНО без контраста (см. шапку): ранг по нему не выдаём.
cs: dict[str, float] = {}
per_channel: dict[str, list[dict]] = {}
for channel in ("katakana", "kanji", "loneline"):
fl = a.min_freq if channel != "loneline" else 2
bucket = []
for w, f in freq[channel].items():
if f < fl:
continue
nch = len(chaps[channel][w])
if nch < a.min_chapters:
continue
ch_list = ["ORTHO:" + channel]
if nch >= a.min_chapters:
ch_list.append("DISP")
score = cs.get(w) if channel == "kanji" else None
if score is not None and score >= 3:
ch_list.append("CONTRAST")
bucket.append({
"src": w, "freq": f, "chapters": nch, "channel": channel, "len": len(w),
"contrast_log2": round(score, 2) if score is not None else None,
"channels": ch_list,
})
key = (lambda r: (-(r["contrast_log2"] or -99), -r["freq"], r["src"])) if channel == "kanji" \
else (lambda r: (-r["freq"], r["src"]))
bucket.sort(key=key)
per_channel[channel] = bucket[: a.top] if a.top else bucket
print(f" канал {channel}: всего {len(bucket)} прошло пороги, взято {len(per_channel[channel])}",
file=sys.stderr)
for channel in ("katakana", "kanji", "loneline"):
result.extend(per_channel[channel])
note = (f"lang={a.lang} chunks={len(rows)}"
+ (f" ref={a.ref.name}" if a.ref else "")
+ (f" zh-contrast={a.zh_contrast.name}" if a.zh_contrast else " zh-contrast=НЕТ"))
emit(result, a.out, note)
return 0
if __name__ == "__main__":
raise SystemExit(main())

45
eval/pkg7/minerharness/build.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Собирает minerharness КАК МОДУЛЬ ВНЕ РЕПО (прецедент пакета-6): исходник живёт в eval/ (чтобы был
# ревьюируем и воспроизводим), а go.mod + `replace` материализуются в рабочем каталоге вне git.
# В репозитории второго go.mod не появляется — сборка/vet бэкенда его не видят.
#
# ./build.sh <work-dir> [commit]
#
# work-dir — куда положить модуль и пин-копию бэкенда (скретчпад сессии).
# commit — какой коммит бэкенда пинить (по умолчанию HEAD). Пин обязателен: параллельная
# бэкенд-сессия правит рабочее дерево, и `replace` на него дал бы невоспроизводимый прогон.
set -euo pipefail
WORK="${1:?usage: build.sh <work-dir> [commit]}"
COMMIT="${2:-HEAD}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
SHA="$(git -C "$REPO" rev-parse --short "$COMMIT")"
PIN="$WORK/pinned-$SHA"
MOD="$WORK/minerharness"
mkdir -p "$PIN" "$MOD"
if [ ! -d "$PIN/backend" ]; then
git -C "$REPO" archive "$COMMIT" backend | tar -x -C "$PIN"
fi
cp "$(dirname "${BASH_SOURCE[0]}")/main.go" "$MOD/main.go"
cp "$PIN/backend/go.sum" "$MOD/go.sum"
# Требования зеркалим из go.mod пина, чтобы версии совпали побайтно и `go mod tidy` не понадобился
# (сеть в сборке не участвует: GOFLAGS=-mod=mod, всё берётся из локального модуль-кеша).
{
echo "module textmachine/backend/minerharness"
echo
sed -n '/^go /p' "$PIN/backend/go.mod"
echo
echo "require textmachine/backend v0.0.0"
echo
sed -n '/^require (/,/^)/p' "$PIN/backend/go.mod"
echo
echo "replace textmachine/backend => $PIN/backend"
} > "$MOD/go.mod"
cd "$MOD"
GOFLAGS=-mod=mod GOPROXY=off go build -o "$MOD/minerharness" .
echo "built: $MOD/minerharness (backend pinned at $SHA in $PIN/backend)"

View file

@ -0,0 +1,184 @@
// Command minerharness прогоняет БОЕВОЙ майнер банка (`internal/miner`) по произвольной книге,
// НЕ ТРОГАЯ репозиторий. Прецедент — пакет-6 (`labels/README.md`): отдельный Go-модуль ВНЕ репо с
// `module textmachine/backend/<имя>` и `replace textmachine/backend => <копия>/backend`; имя обязано
// начинаться с `textmachine/backend/`, иначе Go запретит импорт `internal/`.
//
// Отличие от пакета-6, сознательное: `replace` смотрит не в рабочее дерево, а в КОПИЮ бэкенда,
// извлечённую `git archive <commit>` — параллельно идёт бэкенд-сессия пака-20, её незакоммиченные
// правки (в т.ч. `internal/terminology`, `miner_emit.go`) меняли бы результат под руками. Пин к
// коммиту делает прогон воспроизводимым и снимает вопрос «чей это был код».
//
// Вход: JSONL от `eval/pkg7/split_book.py` ({chapter, chunk_idx, source}). Нормализацию делает
// сам харнесс тем же вызовом, что и прод (`text.NormalizeSourceKey`, pipeline/mining.go:57).
// Выход: TSV эмитированной дельты + сводка. Ноль сетевых вызовов, ноль моделей, $0.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"sort"
"strings"
"textmachine/backend/internal/lang"
"textmachine/backend/internal/membank"
"textmachine/backend/internal/miner"
"textmachine/backend/internal/store"
"textmachine/backend/internal/text"
)
type row struct {
Chapter int `json:"chapter"`
ChunkIdx int `json:"chunk_idx"`
Source string `json:"source"`
}
func main() {
var (
in = flag.String("in", "", "JSONL чанков (chapter, chunk_idx, source)")
contrastP = flag.String("contrast", "", "контраст-корпус (jieba dict.txt)")
packRoot = flag.String("langpack", "", "корень langpack (configs/langpacks)")
srcLang = flag.String("src", "zh", "язык исходника для langpack")
tgtLang = flag.String("tgt", "ru", "целевой язык для langpack")
seedPath = flag.String("seed", "", "опциональный сид книги (YAML); пусто = майнить с нуля")
outTSV = flag.String("out", "", "куда писать TSV дельты (пусто = stdout)")
outYAML = flag.String("out-yaml", "", "опционально: карта подписи в форме DeltaYAML")
)
flag.Parse()
if *in == "" || *contrastP == "" || *packRoot == "" {
fmt.Fprintln(os.Stderr, "usage: minerharness -in chunks.jsonl -contrast dict.txt -langpack <root> [-src zh] [-seed seed.yaml] [-out out.tsv]")
os.Exit(2)
}
chunks, chars, chapters, err := readChunks(*in)
if err != nil {
fatal(err)
}
cf, err := os.Open(*contrastP)
if err != nil {
fatal(err)
}
defer cf.Close()
contrast, err := miner.LoadContrast(cf)
if err != nil {
fatal(err)
}
pack, err := lang.Load(*packRoot, *srcLang, *tgtLang)
if err != nil {
fatal(err)
}
var seed []store.GlossaryEntry
if *seedPath != "" {
seed, err = membank.LoadGlossarySeed(*seedPath)
if err != nil {
fatal(err)
}
}
mined := miner.MineBank(chunks, contrast, seed, nil, miner.FrozenConfig(), pack)
w := os.Stdout
if *outTSV != "" {
f, err := os.Create(*outTSV)
if err != nil {
fatal(err)
}
defer f.Close()
w = f
}
bw := bufio.NewWriter(w)
defer bw.Flush()
fmt.Fprintln(bw, "src\ttype\tfreq\tsince_ch\taliases\tevidence")
byType := map[string]int{}
byLen := map[int]int{}
for _, m := range mined {
byType[m.Type]++
byLen[len([]rune(m.Src))]++
fmt.Fprintf(bw, "%s\t%s\t%d\t%d\t%s\t%s\n",
m.Src, m.Type, m.Freq, m.SinceCh,
strings.Join(m.Aliases, "|"), strings.Join(m.Evidence, "|"))
}
bw.Flush()
if *outYAML != "" {
y, err := miner.DeltaYAML(mined, nil)
if err != nil {
fatal(err)
}
if err := os.WriteFile(*outYAML, []byte(y), 0o644); err != nil {
fatal(err)
}
}
fmt.Fprintf(os.Stderr, "chunks=%d chapters=%d chars=%d pack=%s emitted=%d\n",
len(chunks), chapters, chars, pack.Version(), len(mined))
fmt.Fprintf(os.Stderr, "by_type: %s\n", kvSorted(byType))
fmt.Fprintf(os.Stderr, "by_srclen: %s\n", kvIntSorted(byLen))
}
func readChunks(path string) (out []miner.Chunk, chars, chapters int, err error) {
f, err := os.Open(path)
if err != nil {
return nil, 0, 0, err
}
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 1<<20), 1<<26)
seen := map[int]bool{}
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
var r row
if err := json.Unmarshal([]byte(line), &r); err != nil {
return nil, 0, 0, err
}
chars += len([]rune(r.Source))
seen[r.Chapter] = true
out = append(out, miner.Chunk{
Chapter: r.Chapter, ChunkIdx: r.ChunkIdx,
NSource: text.NormalizeSourceKey(r.Source),
})
}
return out, chars, len(seen), sc.Err()
}
func kvSorted(m map[string]int) string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for i, k := range keys {
if i > 0 {
b.WriteString(" ")
}
fmt.Fprintf(&b, "%s=%d", k, m[k])
}
return b.String()
}
func kvIntSorted(m map[int]int) string {
keys := make([]int, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
var b strings.Builder
for i, k := range keys {
if i > 0 {
b.WriteString(" ")
}
fmt.Fprintf(&b, "%d=%d", k, m[k])
}
return b.String()
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "minerharness:", err)
os.Exit(1)
}

207
eval/pkg7/rubric_probe.py Normal file
View file

@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Полигон, пакет-7 (A2): проба ИЗМЕРИМОСТИ критериев рубрики «нормально переведённый термин».
Рубрика без работающей измерялки это мнение. Скрипт берёт три критерия, про которые
правдоподобно, что они машинные, и проверяет их НА ПОДПИСАННОМ ВЛАДЕЛЬЦЕМ сиде то есть на
эталоне, а не на выдумке. Что сходится с эталоном, то в фазе B можно мерить бесплатно; что не
сходится честно уезжает к человеку.
Критерии в пробе:
M1 морфологическая пригодность склоняется ли dst по-русски, устойчив ли род; сверка
рукописных `decl.forms` сида с формами, которые порождает pymorphy3 (расхождение = либо
ошибка сида, либо предел автомата обе находки полезны);
M2 транскрипционная норма латиница/цифры внутри dst, недопустимые для кириллического
таргета сочетания, а также согласие с фонотактикой Палладия из боевого langpack;
M3 внутренняя однозначность банка один src с несколькими dst и один dst у нескольких src
(коллизии, которые терминологу придётся разрешать по D39.42 п.1).
$0: pymorphy3 (локальный словарь) + stdlib. Ни одного сетевого вызова.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
import yaml
try:
import pymorphy3
except ImportError: # pragma: no cover
pymorphy3 = None
CASES = ["gent", "datv", "accs", "ablt", "loct"]
LATIN = re.compile(r"[A-Za-z]")
DIGIT = re.compile(r"[0-9]")
def head_index(morph, tokens: list[str], typ: str) -> int:
"""Индекс склоняемой вершины словосочетания.
Правило РАЗНОЕ по типу терма, и это само по себе требование к рубрике:
- имя/прозвище («Фан Юань», «Гуюэ Мочэнь», «матушка Шэнь»): вершина ПОСЛЕДНИЙ
компонент антропонима кроме случая «нарицательное + оним» («матушка Шэнь»), где
склоняется нарицательное, а оним стоит неизменным;
- нарицательное словосочетание («деревня Гуюэ», «море истинной ци», «Четвёртый глава
рода»): вершина ПЕРВОЕ существительное, остальное зависимое.
Первая версия скрипта брала последний токен всегда и на «Монах Цветочного Вина» склоняла
«Вина» цифра согласия вышла заниженной по МОЕЙ вине, а не по вине сида.
"""
if not tokens:
return 0
def is_noun(tok: str) -> bool:
if not morph:
return False
p = morph.parse(tok)[0]
return str(p.tag.POS) in ("NOUN", "ADJF", "PRTF")
lower_common = [i for i, t in enumerate(tokens) if t[:1].islower()]
if typ in ("name", "nickname"):
# «матушка Шэнь» — нарицательное впереди строчной буквой ⇒ склоняем его
if lower_common and lower_common[0] == 0:
return 0
return len(tokens) - 1
for i, t in enumerate(tokens):
if is_noun(t):
return i
return 0
def m1_morphology(morph, dst: str, declared: list[str], invariant: bool, typ: str = "") -> dict:
"""Склоняемость, род и сверка объявленных форм с порождёнными."""
tokens = dst.split()
hi = head_index(morph, tokens, typ)
head = tokens[hi] if tokens else dst
parses = morph.parse(head) if morph else []
if not parses:
return {"parsed": False, "head": head}
p = parses[0]
# Слово, которого нет в словаре, pymorphy3 разбирает предсказателем — вердикт по нему
# держать наравне со словарным нельзя, поэтому факт помечается отдельно.
guessed = not getattr(p.methods_stack[0][0], "__class__", type(None)).__name__.startswith("Dictionary")
gen = {}
for c in CASES:
f = p.inflect({c})
if f:
parts = list(tokens)
parts[hi] = f.word
gen[c] = " ".join(parts)
declinable = len(set(gen.values())) > 1
dec_set = {d.strip().lower() for d in declared}
gen_set = {g.lower() for g in gen.values()}
return {
"parsed": True,
"head": head,
"guessed": guessed,
"pos": str(p.tag.POS),
"gender": str(p.tag.gender),
"animacy": str(p.tag.animacy),
"declinable_by_morph": declinable,
"declared_invariant": invariant,
# Расхождение №1: сид говорит «склоняется», автомат не склоняет (или наоборот)
"invariance_conflict": (invariant and declinable) or (not invariant and not declinable),
"declared_forms": sorted(dec_set),
"generated_forms": sorted(gen_set),
"forms_agree": bool(dec_set) and dec_set == gen_set,
"forms_declared_not_generated": sorted(dec_set - gen_set),
"forms_generated_not_declared": sorted(gen_set - dec_set),
}
def m2_translit(dst: str, pack_dir: Path | None) -> dict:
bad = []
if LATIN.search(dst):
bad.append("латиница внутри dst")
if DIGIT.search(dst):
bad.append("цифра внутри dst")
if pack_dir:
pt = pack_dir / "palladius-phonotactics.txt"
if pt.exists():
for line in pt.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) >= 2 and parts[0] and parts[0] in dst.lower():
bad.append(f"фонотактика Палладия: «{parts[0]}» → ожидается «{parts[1]}»")
return {"violations": bad}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--seed", required=True, type=Path)
ap.add_argument("--langpack-pair", type=Path, help="каталог пары langpack (для Палладия)")
ap.add_argument("--out", type=Path)
a = ap.parse_args()
if pymorphy3 is None:
print("pymorphy3 не установлен — критерий M1 не измеряется", file=sys.stderr)
morph = pymorphy3.MorphAnalyzer() if pymorphy3 else None
doc = yaml.safe_load(a.seed.read_text(encoding="utf-8")) or {}
terms = doc.get("terms") or []
rows, by_src, by_dst = [], defaultdict(set), defaultdict(set)
for t in terms:
src, dst = t.get("src", ""), (t.get("dst") or "")
if not dst:
continue
decl = t.get("decl") or {}
r = {
"src": src, "dst": dst, "type": t.get("type", ""), "status": t.get("status", ""),
"M1": m1_morphology(morph, dst, decl.get("forms") or [], bool(decl.get("invariant")), t.get("type", "")),
"M2": m2_translit(dst, a.langpack_pair),
}
rows.append(r)
by_src[src].add(dst)
by_dst[dst.lower()].add(src)
n = len(rows)
parsed = [r for r in rows if r["M1"].get("parsed")]
guessed = [r for r in rows if r["M1"].get("guessed")]
with_forms = [r for r in rows if r["M1"].get("declared_forms")]
agree = [r for r in with_forms if r["M1"]["forms_agree"]]
# Вердикт по неизменяемости имеет силу ТОЛЬКО когда вершина словарная: на угаданной
# («гу», «Чилянь», «ци») автомат выдумывает парадигму, и его «склоняется» ничего не значит.
conflict = [r for r in rows
if r["M1"].get("invariance_conflict") and not r["M1"].get("guessed")]
conflict_guessed = [r for r in rows
if r["M1"].get("invariance_conflict") and r["M1"].get("guessed")]
m2bad = [r for r in rows if r["M2"]["violations"]]
coll_src = {k: v for k, v in by_src.items() if len(v) > 1}
coll_dst = {k: v for k, v in by_dst.items() if len(v) > 1}
print(f"термов с dst: {n}")
print(f"M1 разобрано pymorphy3: {len(parsed)}/{n}; из них ВЕРШИНА УГАДАНА "
f"предсказателем (нет в словаре): {len(guessed)}")
print(f"M1 объявлены формы в сиде: {len(with_forms)}; из них СОВПАЛИ с порождёнными: "
f"{len(agree)} ({len(agree) / len(with_forms):.3f})" if with_forms else "M1 форм нет")
print(f"M1 конфликт «invariant vs склоняемость», вершина СЛОВАРНАЯ (вердикт имеет силу): "
f"{len(conflict)}")
print(f"M1 то же, но вершина УГАДАНА (вердикт силы НЕ имеет, показан справочно): "
f"{len(conflict_guessed)}")
print(f"M2 нарушений (латиница/цифра/фонотактика): {len(m2bad)}")
print(f"M3 один src → несколько dst: {len(coll_src)} {dict(list(coll_src.items())[:5])}")
print(f"M3 один dst → несколько src: {len(coll_dst)} {dict(list(coll_dst.items())[:5])}")
print("\nM1: где рукописные формы РАЗОШЛИСЬ с автоматом (первые 15):")
for r in [r for r in with_forms if not r["M1"]["forms_agree"]][:15]:
m = r["M1"]
print(f" {r['src']}{r['dst']} сид={m['forms_declared_not_generated']} "
f"автомат={m['forms_generated_not_declared']}")
print("\nM1: конфликт объявленной неизменяемости (первые 15):")
for r in conflict[:15]:
m = r["M1"]
print(f" {r['src']}{r['dst']} invariant={m['declared_invariant']} "
f"склоняем_автоматом={m['declinable_by_morph']} pos={m['pos']} род={m['gender']}")
if a.out:
a.out.write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())

100
eval/pkg7/seed_recall.py Normal file
View file

@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Полигон, пакет-7 (A3): recall боевого WHICH-канала против ПОДПИСАННОГО ВЛАДЕЛЬЦЕМ банка.
Вопрос, на который отвечает скрипт: из терминов, которые владелец УЖЕ подписал для этой книги и
которые физически встречаются в срезе, сколько боевой майнер предлагает сам?
Дисциплина замера (иначе цифра врёт в свою пользу):
- знаменатель только те термины сида, что РЕАЛЬНО встречаются в срезе (иначе меряем не recall,
а длину среза);
- майнер прогоняется с ПУСТЫМ сидом, иначе он по построению исключает уже засеянное
(`miner_emit.go:134` `seedSurfaces[c.Src]` continue) и recall выйдет 0 по определению;
- засчитывается и попадание в алиас-кластер: движок считает такой кандидат «алиасом
существующего» и владелец видит его на стопе. Обе цифры печатаются раздельно.
- ключи нормализуются тем же способом, что и в движке (tradsimp, NFKC, lower) тут повторено
ЧАСТИЧНО (NFKC+casefold): полный `text.NormalizeSourceKey` Go. Расхождение помечается.
$0, stdlib + PyYAML.
"""
from __future__ import annotations
import argparse
import json
import sys
import unicodedata
from pathlib import Path
import yaml
def nk(s: str) -> str:
return unicodedata.normalize("NFKC", s).casefold().strip()
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--seed", required=True, type=Path)
ap.add_argument("--chunks", required=True, type=Path)
ap.add_argument("--mined-tsv", required=True, type=Path)
ap.add_argument("--out", type=Path)
a = ap.parse_args()
doc = yaml.safe_load(a.seed.read_text(encoding="utf-8")) or {}
terms = doc.get("terms") or []
text = "\n".join(json.loads(l)["source"] for l in a.chunks.open(encoding="utf-8") if l.strip())
mined_primary, mined_all = set(), set()
for i, line in enumerate(a.mined_tsv.open(encoding="utf-8")):
if i == 0:
continue
f = line.rstrip("\n").split("\t")
if not f or not f[0]:
continue
mined_primary.add(nk(f[0]))
mined_all.add(nk(f[0]))
if len(f) > 4 and f[4]:
mined_all.update(nk(x) for x in f[4].split("|") if x)
rows = []
for t in terms:
src = t.get("src") or ""
surfaces = [src] + [al.get("alias", "") for al in (t.get("aliases") or [])]
present = [s for s in surfaces if s and s in text]
if not present:
continue
keys = {nk(s) for s in surfaces if s}
rows.append({
"src": src, "type": t.get("type", ""), "dst": t.get("dst", ""),
"status": t.get("status", ""),
"occurrences": sum(text.count(s) for s in present),
"hit_primary": bool(keys & mined_primary),
"hit_any": bool(keys & mined_all),
})
n = len(rows)
hp = sum(r["hit_primary"] for r in rows)
ha = sum(r["hit_any"] for r in rows)
bytype: dict[str, list[int]] = {}
for r in rows:
b = bytype.setdefault(r["type"] or "", [0, 0])
b[0] += 1
b[1] += int(r["hit_any"])
print(f"термов сида, встречающихся в срезе: {n}")
print(f" предложены майнером как ПЕРВИЧНЫЙ терм: {hp} recall={hp / n:.3f}" if n else "")
print(f" предложены как терм ИЛИ алиас: {ha} recall={ha / n:.3f}" if n else "")
print("по типам (знаменатель / попало):")
for k in sorted(bytype):
c, h = bytype[k]
print(f" {k:8s} {h:3d}/{c:<3d} = {h / c:.3f}")
print("\nНЕ предложенные (топ-25 по частоте):")
for r in sorted((r for r in rows if not r["hit_any"]), key=lambda r: -r["occurrences"])[:25]:
print(f" {r['src']}\t{r['type']}\t{r['occurrences']}×\t{r['dst']}")
if a.out:
a.out.write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())

251
eval/pkg7/split_book.py Normal file
View file

@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""Полигон, пакет-7 (A3): нарезка книг стенда в общий JSONL-субстрат для майнинга.
Выход одна строка на чанк: {"chapter": int, "chunk_idx": int, "source": str}. Это ровно тот
контракт, который `internal/miner.Chunk` ждёт от вызывающего (см. pipeline/mining.go:55-58
Chapter/ChunkIdx/NSource), поэтому боевой Go-майнер и мои собственные скрипты по en/ja едят
ОДИН И ТОТ ЖЕ субстрат и их выходы сравнимы.
ЧЕСТНАЯ ОГОВОРКА (пре-регистрирована до прогонов): это НЕ боевой чанкер. Боевой режет по
output-бюджету модели (`internal/chunk`), здесь же по абзацам до ~target символов внутри главы.
Для майнера различие затрагивает только границы n-грамм и пер-чанковый счёт вхождений, поэтому
эффект замерян отдельно (--target-свип), а не объявлен пренебрежимым.
$0: ни одного сетевого вызова, ни одной модели.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import unicodedata
import zipfile
from html.parser import HTMLParser
from pathlib import Path
# --- маркеры глав по книгам стенда -------------------------------------------------------------
# Каждое правило — (regex, группа с номером, парсер номера). Правила ДАННЫЕ, а не ветвление по книге
# в коде: добавить книгу = добавить строку.
CJK_NUM = {"": 0, "": 0, "": 1, "": 2, "": 3, "": 4, "": 5,
"": 6, "": 7, "": 8, "": 9}
JP_FULLWIDTH = {chr(0xFF10 + i): str(i) for i in range(10)}
def cjk_int(s: str) -> int:
"""Китайская/японская запись числа → int (十/百/千 позиционно). Только то, что реально
встречается в заголовках глав: до 9999."""
s = s.strip()
if s.isdigit():
return int(s)
if all(c in JP_FULLWIDTH for c in s):
return int("".join(JP_FULLWIDTH[c] for c in s))
total, section, digit = 0, 0, 0
for ch in s:
if ch in CJK_NUM:
digit = CJK_NUM[ch]
elif ch == "":
section += (digit or 1) * 10
digit = 0
elif ch == "":
section += (digit or 1) * 100
digit = 0
elif ch == "":
section += (digit or 1) * 1000
digit = 0
elif ch == "":
total += (section + digit) * 10000
section = digit = 0
else:
return -1
return total + section + digit
EN_ORD = ("one two three four five six seven eight nine ten eleven twelve thirteen fourteen "
"fifteen sixteen seventeen eighteen nineteen twenty").split()
def en_int(s: str) -> int:
s = s.strip().lower().replace("-", " ")
parts = s.split()
if len(parts) == 1 and parts[0] in EN_ORD:
return EN_ORD.index(parts[0]) + 1
if len(parts) == 2 and parts[0] == "twenty" and parts[1] in EN_ORD:
return 20 + EN_ORD.index(parts[1]) + 1
if s.isdigit():
return int(s)
return -1
RULES = {
# 蛊真人: `第1节 …` / `第一节 …` — тот же маркер/юнит, что читает боевой langpack (heading.txt: 第 + 章节節回)
"guzhenren": (re.compile(r"^\s*第\s*([0-9]+|[〇零一二三四五六七八九十百千万]+)\s*[节節章]"), cjk_int),
# 金瓶梅 (ctext-срез): `===== 第4回 =====`
"jinpingmei": (re.compile(r"^=+\s*第\s*([0-9]+)\s*回\s*=+"), cjk_int),
# 異世界魔術師 (syosetu-дамп): `001. 第1話`
"isekai": (re.compile(r"^\s*[0-9]{3}\.\s*第\s*([0-9-]+)\s*話"), cjk_int),
# Fifty Shades (txt): `CHAPTER ONE` в теле (в оглавлении — ` Chapter One`, отсекается регистром)
"fifty": (re.compile(r"^CHAPTER\s+([A-Z\-]+)\s*$"), en_int),
# Empire of the Dawn (epub): заголовки внутри xhtml не структурны — режем по файлам сплита
"epub-files": (None, None),
}
class _Text(HTMLParser):
"""Минимальный html→text без внешних зависимостей (bs4/ebooklib в venv полигона нет)."""
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.parts: list[str] = []
self._skip = 0
def handle_starttag(self, tag, attrs):
if tag in ("script", "style"):
self._skip += 1
elif tag in ("p", "div", "br", "h1", "h2", "h3", "h4", "li"):
self.parts.append("\n")
def handle_endtag(self, tag):
if tag in ("script", "style") and self._skip:
self._skip -= 1
elif tag in ("p", "div", "h1", "h2", "h3", "h4", "li"):
self.parts.append("\n")
def handle_data(self, data):
if not self._skip:
self.parts.append(data)
def text(self) -> str:
return re.sub(r"\n{3,}", "\n\n", "".join(self.parts))
def read_epub(path: Path) -> list[tuple[int, str]]:
"""EPUB → [(порядковый номер документа, текст)] в порядке spine из content.opf."""
with zipfile.ZipFile(path) as z:
opf_name = next((n for n in z.namelist() if n.endswith(".opf")), None)
order: list[str] = []
if opf_name:
opf = z.read(opf_name).decode("utf-8", "replace")
base = str(Path(opf_name).parent)
ids = dict(re.findall(r'<item\s[^>]*id="([^"]+)"[^>]*href="([^"]+)"', opf))
ids.update({i: h for h, i in re.findall(
r'<item\s[^>]*href="([^"]+)"[^>]*id="([^"]+)"', opf)})
for idref in re.findall(r'<itemref[^>]*idref="([^"]+)"', opf):
href = ids.get(idref)
if not href:
continue
name = href if base in ("", ".") else f"{base}/{href}"
if name in z.namelist():
order.append(name)
if not order:
order = sorted(n for n in z.namelist()
if n.lower().endswith((".xhtml", ".html", ".htm")))
raw = []
for i, name in enumerate(order, 1):
p = _Text()
p.feed(z.read(name).decode("utf-8", "replace"))
t = p.text().strip()
if t:
raw.append((i, t))
# Колонтитулы epub: строка, повторяющаяся в >30% документов сплита, — это шапка/подвал
# (серия, автор, название), а не текст. Первый прогон без этого фильтра вывел «dawn» и
# «Vampire Empire» в верхушку кандидатов с рассеянием 125/126 глав — чистый артефакт ингеста.
from collections import Counter as _C
docfreq: _C = _C()
for _, t in raw:
for ln in {x.strip() for x in t.split("\n") if x.strip()}:
docfreq[ln] += 1
boiler = {ln for ln, c in docfreq.items() if c > 0.3 * len(raw)}
out = []
for i, t in raw:
kept = "\n".join(ln for ln in t.split("\n") if ln.strip() not in boiler)
if kept.strip():
out.append((i, kept))
if boiler:
print(f" epub: отброшено {len(boiler)} колонтитульных строк "
f"(порог >30% документов): {sorted(boiler)[:5]}", file=sys.stderr)
return out
def paragraphs(body: str) -> list[str]:
"""Абзацы: непустые строки. Мягкая нормализация пробелов, содержимое не трогаем."""
return [ln.strip() for ln in body.replace("\r\n", "\n").split("\n") if ln.strip()]
def pack(paras: list[str], target: int) -> list[str]:
"""Склейка абзацев в чанки ~target символов (детерминированно, жадно)."""
chunks, cur, n = [], [], 0
for p in paras:
if cur and n + len(p) > target:
chunks.append("\n".join(cur))
cur, n = [], 0
cur.append(p)
n += len(p)
if cur:
chunks.append("\n".join(cur))
return chunks
def split_text(text: str, rule_name: str, target: int, max_ch: int) -> list[dict]:
rx, num = RULES[rule_name]
lines = text.replace("\r\n", "\n").split("\n")
chapters: list[tuple[int, list[str]]] = []
cur_no = 0
cur: list[str] = []
for ln in lines:
m = rx.match(ln) if rx else None
if m:
n = num(m.group(1))
if n > 0:
if cur:
chapters.append((cur_no, cur))
cur_no, cur = n, []
continue
cur.append(ln)
if cur:
chapters.append((cur_no, cur))
# Пролог/оглавление (глава 0) отбрасываем: у 蛊真人 это аннотация, у Fifty Shades — оглавление,
# и в обоих случаях это не тело книги. Решение принято ДО замеров и одинаково для всех книг.
chapters = [(n, b) for n, b in chapters if n > 0]
if max_ch:
chapters = [c for c in chapters if c[0] <= max_ch]
out = []
for no, body in chapters:
for idx, ch in enumerate(pack(paragraphs("\n".join(body)), target)):
out.append({"chapter": no, "chunk_idx": idx, "source": ch})
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--book", required=True, type=Path)
ap.add_argument("--rule", required=True, choices=sorted(RULES))
ap.add_argument("--target", type=int, default=2000, help="целевой размер чанка в символах")
ap.add_argument("--max-chapters", type=int, default=0)
ap.add_argument("--out", required=True, type=Path)
a = ap.parse_args()
if a.rule == "epub-files":
docs = read_epub(a.book)
if a.max_chapters:
docs = docs[: a.max_chapters]
rows = []
for no, body in docs:
for idx, ch in enumerate(pack(paragraphs(body), a.target)):
rows.append({"chapter": no, "chunk_idx": idx, "source": ch})
else:
raw = a.book.read_text(encoding="utf-8", errors="replace")
rows = split_text(raw, a.rule, a.target, a.max_chapters)
a.out.parent.mkdir(parents=True, exist_ok=True)
with a.out.open("w", encoding="utf-8") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
chars = sum(len(r["source"]) for r in rows)
chaps = len({r["chapter"] for r in rows})
print(f"{a.out}: chunks={len(rows)} chapters={chaps} chars={chars}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())