From 9f2a143a8d595f5ddebc81484385424e3137026a Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sun, 26 Jul 2026 10:08:38 +0300 Subject: [PATCH] Accept polygon package seven phase A as D39.43: miner recall measured, emission-layer attribution, terminologist cost amendment, land report and pkg7 tools --- docs/PROGRESS.md | 8 +- docs/architecture/05-decisions-log.md | 10 + .../POLYGON_TERM_RESEARCH_A_2026-07-26.md | 809 ++++++++++++++++++ eval/pkg7/README.md | 22 + eval/pkg7/kwic.py | 63 ++ eval/pkg7/mine_nonhan.py | 285 ++++++ eval/pkg7/minerharness/build.sh | 45 + eval/pkg7/minerharness/main.go | 184 ++++ eval/pkg7/rubric_probe.py | 207 +++++ eval/pkg7/seed_recall.py | 100 +++ eval/pkg7/split_book.py | 251 ++++++ 11 files changed, 1982 insertions(+), 2 deletions(-) create mode 100644 docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md create mode 100644 eval/pkg7/README.md create mode 100644 eval/pkg7/kwic.py create mode 100644 eval/pkg7/mine_nonhan.py create mode 100755 eval/pkg7/minerharness/build.sh create mode 100644 eval/pkg7/minerharness/main.go create mode 100644 eval/pkg7/rubric_probe.py create mode 100644 eval/pkg7/seed_recall.py create mode 100644 eval/pkg7/split_book.py diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 2fb8ab6c..c3b36e19 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,12 +1,16 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-07-25, пак-17 закрыт, право по ToS подписано, пак-18 принят — долгов контракта нет, D39.26–D39.31). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.42); этот файл — ЖУРНАЛ.** +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-07-25, пак-17 закрыт, право по ToS подписано, пак-18 принят — долгов контракта нет, D39.26–D39.31). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.43); этот файл — ЖУРНАЛ.** > - **Фазы/курс:** Ф0 ✅ · Ф1-инфра ✅ (D20–D28; golden = инвариант №8) · арка «качество-первым» D26→D38.5 закрыта · **арх-ресет D39 исполнен ЦЕЛИКОМ** (7 слоёв → программа D39.1–D39.10 → паки 11–16, D39.13–D39.24; статус слоёв — шапка-таблица `architecture/09-target-architecture.md`) · пере-прогон rerun2 прочитан (D39.20: операционка живьём, $0-резюм ×3 арма, $6.63<$15; планка ≤2 НЕ пройдена; анти-корреляция гладкость↔верность ×3 → mistral-редактор вон) · эксп ЗАКРЫТ (D39.22), итерация №2 отложена · **курс = разработка бэкенда: пак-17 «канал B вживую» ЗАКРЫТ ЦЕЛИКОМ (дизайн D39.26 · стройка D39.27 · дельта `allow` D39.28, 25.07); вокабуляр = ПАРА `violence`/`sexually-explicit`, остаток — ФАКТЫ ToS в зоне полигона (fail-closed до них)** (D39.25: в движке НЕТ понятия «18+» — generic content-labels × provider-capabilities; лейблы вне хешей · резолв до валидации · один хоп `chain[0]` · гранулярность = книга) → **КУРС АМЕНДИРОВАН ВЛАДЕЛЬЦЕМ 25.07 (D39.33): МАСШТАБ — ПОСЛЕДНИМ, после достройки алгоритмического идеала; доказательство эффективности крупных изменений — МИНИ-ПРОГОНЫ ~10 глав (детерминированные чекеры и скан остатка первыми, судья — только когда от него зависит решение).** **ОЧЕРЕДЬ ИСПРАВЛЕНА ЗАМЕРОМ (D39.35): $0-резюм драфта на стенде УЖЕ МЁРТВ** (снапшот разошёлся по трём осям — cheapgate v3→v4, renderfmt v2→v3, langpack 09974d6→a7d4be2), поэтому срочности «гнать, пока жив» НЕТ, а ниты перестали быть условными: **(1) микро-пак «предпрогонная гигиена» ($0-код): два нита общности + ОБЯЗАТЕЛЬНЫЙ флип `Gates.RegressionGuard.Enabled` (невыполненное обязательство D38.4 п.5) + опц. проекция пере-оплаты в `tmctl status`** · **(2) мини-прогон-замер ~10 глав** (драфт ≈$0.02, редактура ≈$0.08–0.15/арм, судья НЕ зовётся ⇒ один арм ≈$0.10–0.17, два ≈$0.20–0.32; перед прогоном бэкап `rerun2/*.db`, БД на схеме v9 против v10 бинаря) · **(3) параллельно $0 — сид/банк-дельта** (元 · 赤城 · 学堂家老 · 春秋蝉; два терма ждут подписи владельца) · **(4) пак-19 в урезанном скоупе** · (4) далее слой-2 извлечение из тел промтов · ru-target · native-Gemini судья. Затем — сид-дельта (元 · 赤城 · 学堂家老 · 春秋蝉), МАСШТАБ целой книги (7,78 млн символов, ~2284 раздела) и пилот Ф2.5. Хроника треков A/B, exp15/16 и стройки паков — архивы ниже + D-лог. > - **Стек:** draft deepseek-v4-flash (thinking ON) → **editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (D39.22; glm-5 резерв, mistral вон D39.20)**, промпт v3-discourse (P1a+чэнъюй+few_shot-тумблер) → судья gemini-3.1-pro-preview; канал B Mistral+grok; 7 ключей; ~$0.85/ранобэ (D30.4). -> - **ОТКРЫТЫЕ ПЕТЛИ (норма 25.07, перепись 180 инцидентов: класс NEVER_CLOSED больше не копим — каждая петля обязана получить диспозицию решено/отложено-с-записью/отклонено; ведёт оркестратор).** *Владелец:* ~~вокабуляр content-лейблов~~ **РЕШЕНО 25.07: ПАРА `violence`/`sexually-explicit`** (D39.27 п.5 — сохраняет D14 п.1 и dspro-редактора на violence) ⇒ следствие в работе: assert-only `action` (дельта пака-17) · ~~Q2 редактор под лейблом~~ **ОТЛОЖЕН С ЗАПИСЬЮ** (D39.27 п.7: при паре связывает только `sexually-explicit`-книги, которых нет; возвращается вместе с предусловием D22.7 перед первой explicit-книгой; кандидаты — grok-4.3 интерим, glm-5 только после сверки ToS Z.AI по первоисточнику, mistral отвергнут D39.20) · ~~ToS-факты + три решения по ним~~ **ЗАКРЫТЫ 25.07 (D39.29 факты + D39.30 подписи владельца):** лейбл `violence` НЕ заводится, интерпретация «нейтральное изображение ≠ пропаганда» зафиксирована письменно · риск Z.AI (плоский запрет «violent content»; экспозиция состоялась в rerun2 через glm-арм) ПРИНЯТ сознательно с условиями пересмотра · две строки `accepts_labels: [sexually-explicit]` (`xai`, `mistral`) ПОДПИСАНЫ и ПРИМЕНЕНЫ в `models.yaml`. **Остаток ToS-вопросов ЗАКРЫТ владельцем 25.07 (D39.32):** local = «политик нет, переводим всё» ⇒ строка применена · Gemini-оговорка читается СУЖЕНИЕМ ⇒ вердикт `FORBIDDEN`→`UNCLEAR`, но по доктрине это НЕ разрешение: строку Gemini не получает, **ждёт одного слова, если владелец хочет подписать интерпретацию как разрешение** · причину пустоты в конфиг НЕ выносим (живёт в quirks-доке) · **ре-чек политик стал ПРАВИЛОМ** (триггеры: Google ToS 30.07.2026 · квартал 25.10.2026 · любая правка `accepts_labels`) · дата аккаунта OpenAI — открыта, приоритет низкий (OpenAI вне цепочки). **Продуктовое (Ф3):** Gemini API Additional ToS запрещает клиентов, «likely to be accessed by individuals under 18» — обязательство на конечный продукт, независимо от лейблов · **промпт-слой под лейблом (НОВОЕ, вскрыто вопросом владельца): лейбл сегодня НЕ доезжает до промпта** (чистые данные маршрутизации + load-time гарантия); нужно ли, чтобы переводчик/редактор ЗНАЛИ о свойстве контента — отдельное решение промпт-слоя · ~~мини-каноны сид-дельты~~ ЗАКРЫТО 26.07: все четыре пункта решены, правок сида ноль; книжные канон-решения живут С КНИГОЙ (`books/gu-zhenren/CANON-NOTES.md`), НЕ в D-логе — урок владельца: движок не строится вокруг одной книги · ~~включение платной петли ремонта~~ **РЕШЕНО 26.07 (D39.38): НЕ включается** — замер 0 в её классах; вернуться при расширении классов и ненулевом остатке; **уточнение D39.39: «0» = ноль в охвате детекторов с измеренным теперь recall (латиница 0.50 боевой, битые формы 0.10) — решение в силе (петля видит теми же детекторами), но читать как «финал чист» нельзя** · **вопросы разметки Р1–Р4 (НОВОЕ, D39.39, пакет-6 §5, 21 спорная строка):** гипербола 千万 (довод: тот же автор в гл.2 даёт 数十万 о том же деянии) · что есть дефект в классе 成 — форма или значение (от ответа precision K5c 1.000 или 0.167) · сокращённая форма термина («гу» при dst «гу-тварь») · принципиально неразрешимая офлайн речь/мысль (если да — потолок recall K4b < 1.0 by construction, вписать в контракт правила) · ~~место фикс-пака «чекеры» в очереди~~ **РЕШЕНО 26.07 (D39.40): это ПАК-21, а перед ним мини-прогон ~10 глав на полном бэкенде (после лендинга 20 и 19)**; ответы Р1–Р4 нужны к дизайну пака-21 · ja→ru-реплика §B5 (тайминг) · старые Ф2.5-блокеры (билингв-якорь — D25 п.9 Q1 · контаминация корпуса — D27 п.4 · судья-дублёр D22.6 · планка запуска · publishable/waiver — D25 п.1 · FN-bound L3 — D25 п.4 · юр-пакет · провенанс 12-*-доков · xAI-ключ off перед продом D27 · residual-тачпойнты exp16, не влитые в сид-дельту: мини-голд алиасов · precision@30, `books/gu-zhenren/exp16/`). *Оркестратор:* ~~ратификация дизайна пака-16~~ РЕШЕНО D39.24 (25.07) · фантом-гард D-ссылок в чек-листе лендинга (действует) · ~~doc-sync pass~~ **ИСПОЛНЕН 25.07** (запись ниже: оба puml перерисованы под пост-пак-16 · баннеры на 01/02/03/04/06/07/08/09 · ресёрч-свип 18/18c/19/20/21/22 · README/POLYGON_PACKAGE4 освежены; residual — пинги полигону в его зоне) · ~~приёмка пака-17 фаза 1~~ **РЕШЕНО D39.26 (25.07: принято в редакции §14, три добора приёмки, фаза 2 размечена)** · **`escalation_model` = ОТВЕТИВШАЯ модель (НОВОЕ, вынесено из пака-17): вердикт-изменение** — golden пинит `esc_model` на ОТКАЗАВШЕМ хопе, маскировка не скрывает ⇒ отдельная ратификация с санкционированным пере-капчером · **`--accept-rebill[=usd]` с порогом `min($0.50, 5%×ProjectedBookUSD)` РАТИФИЦИРОВАН D20.2-Q2, но в коде/CLI ОТСУТСТВУЕТ (НОВОЕ)** — живёт только в спеке `backend/docs/D15.2-*.md`; `--resnapshot` пере-пиннит без суммы · **D22.7 (пер-чанковый L3-скрин) — предусловие первой erotica-книги в проде НЕ снято паком-17 (НОВОЕ, держать отдельным гейтом)** · фантом-гард D-ссылок в чек-листе лендинга (действует). *Бэкенд:* **ХВОСТЫ МИНИ-ПРОГОНА (D39.37, 26.07, по убыванию веса):** ~~(1) размеченный набор для чекеров~~ **ЗАКРЫТ 26.07 (полигон-пакет-6, D39.39)** ⇒ породил вход нового фикс-пака «чекеры»: 12 дефектов с file:line (супрессор K5c гасит юнит целиком · атрибуция K4b (23 безатрибутивных + 12 с `!?…`) · `inner_marker` на тире-строки · предохранитель K5a отсекает все реальные магнитуды · заглавные/гомоглифы K2 · 两 в `cheng_re` · U+0301 рвёт `TokenizeCyrillic` · мн.число в `decl.forms`-тулинге · однознаковые ключи пост-чека) — приёмка против набора за $0; **место РЕШЕНО (D39.40): ПАК-21, после мини-прогона на полном бэкенде; + переезд `translitInterjections` в данные (улов владельца)** · (2) **тест-пин джойна WHAT↔WHICH** (мутация оркестратора выжила — фикс-лист №1 следующего касания) · ~~(3) покрытие WHAT↔WHICH + флаговый майнинг-стоп~~ **ПАК-20: фаза 1 ПРИНЯТА 26.07 (D39.41, верификация 39/7/0), решения владельца РАТИФИЦИРОВАНЫ (D39.42: ядро = роль ТЕРМИНОЛОГ, флаг поверх, редактор ест неподписанное с пометкой, банкнота отдельным файлом, точечная ре-редактура по ключу, веб-фетч НЕ в v1); директива фазы 2 вписана в промт пака — ждёт релея сессии; + два архитектурных задела владельца 26.07 КОНТРАКТАМИ, не стройкой (серийный шаринг банка: подписной глоссарий round-trip как сид следующей книги · майнинг из «оригинал + чужой перевод»: target-агностичный вход терминолога, провенанс различает источник, юр-сторона — юр-пакет Ф2.5)** · (4) `dialogue_dash` мерить только на невиданном тексте (подогнан под 蛊真人) · (5) генеральность нарезки: кана/ханьцзи один класс ⇒ ja `est_out` ~1.7× завышен МОЛЧА; `EstimateTokens` недосчитывает кириллицу (резервации занижены) · (6) golden второй фикстурой С langpack · (7) `prompt_override` — честная формулировка гарантии слоя-2 · (8) майнер/банк не-CJK (едет с ja→ru) · (9) `request_log` скоуп/ретеншен + `first_flag_reason` третье представление истории · (10) флор-16000 без модели · (11) `max_tokens`-упоры вернутся на en · ~~пак-17 (фазы 1–2 + дельта)~~ **ЗАКРЫТ ЦЕЛИКОМ 25.07 (D39.26/27/28)** · ~~ПАК-18 «долги контракта»~~ **ПРИНЯТ И ЗАЛЕНДЕН 25.07 (D39.31) — долгов контракта не осталось**; выдавался (`BACKEND_PACK18_DEBTS_SESSION_PROMPT.md`: `--accept-rebill` на снапшотной гранулярности — `guard_hash` в коде нет и не строим · `escalation_model` = авторитетная модель с САНКЦИОНИРОВАННЫМ пере-капчером golden под этот дифф · два нита общности условно-байт-нейтрально) · **ru-target долг (слой 7, `isRuTarget`×11 прод-сайтов · `TokenizeCyrillic`×6 · ключ «ru» в санитайзере) — ЖДЁТ СВОЕГО ДИЗАЙН-ПАКА** (связывает только не-русский таргет, по текущей карте zh/ja/en→ru не срочен) · **ПАК-19 «голос и состояние» ВЫДАН 25.07** (`BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md`, двухфазный; ниты общности едут ЕГО лендингом — D39.31 п.5) · **проекция пере-оплаты в `tmctl status` (НОВОЕ, из отчёта пака-18): спека §9 предлагает заменить булев `ConfigDrift` числом «N чанков, ~$X» — дёшево, `omitempty`, не сделано сознательно** · **не-долги, следующие несущие:** масштаб целой книги (волны/каденс/потолки/ETA на сотнях глав — гоняли максимум 5) · Ф2-механизмы (голос/состояние D21 · native-Gemini судья) · извлечение дискурс-норм/few-shot из ТЕЛ промтов (слой 2, форсинг = ja→ru) · ~~пак-16 ф.2~~ ЗАЛЕНДЕНА 25.07 · per-class исходы ремонта — схемное решение (класс не durable), принимается ВМЕСТЕ с включением петли · ниты общности (владелец 25.07) — **ОТЛОЖЕНЫ ПО ЗАМЕРУ (D39.31 п.5): едут на ближайший ПОЛНЫЙ `--resnapshot`**; для Detail-строк байт-нейтральной формы не существует определительно (дефолт шаблона обязан рендерить текущую строку). Прежняя формулировка: только в байт-нейтральной форме с доказательством исполнением (иначе `CheapGateVersion`/`pack.Version()` двигают снапшот стендовых книг и убивают $0-резюм драфта под замер остатка пака-16): Detail-строки чекеров → шаблоны/цитаты из данных (статический 时辰-текст в generic-файле соврёт второй паре) · множитель ×2 `lintTimeUnits` (`checkers.go:174`) → ключ данных. *Полигон:* ~~пакет-6 (размеченный набор чекеров)~~ **ОТРАБОТАН И ПРИНЯТ 26.07 (D39.39): 3015 позиций, 6 прогонов, $0; metrics.json воспроизведён побайтно; набор на стенде `books/gu-zhenren/labels/` = постоянный измерительный стенд чекеров; **ПАКЕТ-7 ВЫДАН 26.07** (`POLYGON_PACKAGE7_TERM_SOURCES_SESSION_PROMPT.md`, параллельно фазе 2 пака-20, ≤$1): жанровый словарь-драфт на подпись владельцу + ресёрч веб-фетча терминолога (источники/провенанс/мини-эксперимент вслепую по D39.42 п.2)** (оставшиеся кандидаты: ёфикатор/санитайзер-классы не размечены — §6.5 отчёта пакета-6; `dialogue_dash` на невиданном тексте) · ~~пакет 5 (факты ToS + 8 пингов)~~ **ОТРАБОТАН ЦЕЛИКОМ И ПРИНЯТ 25.07 (D39.29); все восемь пингов закрыты.** Исторический список закрытых пингов: (1) `00-provider-quirks.md:13,73` — катовер deepseek-chat 24.07 ПРОШЁЛ, пост-катовер факт (жив ли алиас, цены) не внесён — live-проба `/models` + вендор-дока; (2) `00-provider-quirks.md:69` — оговорка «mistral-editor-арм до guard» superseded D39.20 (mistral вон из редакторов; rate-limit-цифры оставить — нужны переводчику канала B пака-17); (3) `09-pilot-protocol.md:3` — ⚠-указатель застрял на D31/glm-5: продлить до пост-D39.22 (dspro-интерим · планка ≤2 не пройдена rerun2 · авто-QA слепыми метками D39.20 · стайл-каноны D39.21), тело не трогать; (4) `15-segmentation-empirics.md:6-7` — шапка до сих пор «НИ ОДНОГО платного вызова», а эксп залендён REV.2 (D39.7/D39.8, $12.79) — перевернуть статус-блок; (5) `16-bank-mining.md:3` — дописать финал «ИСПОЛНЕН И ЗАЛЕНДЁН (D39.10); gender-тачпойнт §7 закрыт (D39.19); продолжение — Go-майнер парити EXACT»; (6) `experiments/README.md` — индекс без строк exp15/16 (сверка застряла на D38.2); (7) `eval/README.md` — не тронут с 19.07 (пре-rerun2): освежить статус под пост-D39.22 (эксп закрыт · судейский риг-стандарт D39.7 · экстракция только `tmctl export`). +> - **ОТКРЫТЫЕ ПЕТЛИ (норма 25.07, перепись 180 инцидентов: класс NEVER_CLOSED больше не копим — каждая петля обязана получить диспозицию решено/отложено-с-записью/отклонено; ведёт оркестратор).** *Владелец:* ~~вокабуляр content-лейблов~~ **РЕШЕНО 25.07: ПАРА `violence`/`sexually-explicit`** (D39.27 п.5 — сохраняет D14 п.1 и dspro-редактора на violence) ⇒ следствие в работе: assert-only `action` (дельта пака-17) · ~~Q2 редактор под лейблом~~ **ОТЛОЖЕН С ЗАПИСЬЮ** (D39.27 п.7: при паре связывает только `sexually-explicit`-книги, которых нет; возвращается вместе с предусловием D22.7 перед первой explicit-книгой; кандидаты — grok-4.3 интерим, glm-5 только после сверки ToS Z.AI по первоисточнику, mistral отвергнут D39.20) · ~~ToS-факты + три решения по ним~~ **ЗАКРЫТЫ 25.07 (D39.29 факты + D39.30 подписи владельца):** лейбл `violence` НЕ заводится, интерпретация «нейтральное изображение ≠ пропаганда» зафиксирована письменно · риск Z.AI (плоский запрет «violent content»; экспозиция состоялась в rerun2 через glm-арм) ПРИНЯТ сознательно с условиями пересмотра · две строки `accepts_labels: [sexually-explicit]` (`xai`, `mistral`) ПОДПИСАНЫ и ПРИМЕНЕНЫ в `models.yaml`. **Остаток ToS-вопросов ЗАКРЫТ владельцем 25.07 (D39.32):** local = «политик нет, переводим всё» ⇒ строка применена · Gemini-оговорка читается СУЖЕНИЕМ ⇒ вердикт `FORBIDDEN`→`UNCLEAR`, но по доктрине это НЕ разрешение: строку Gemini не получает, **ждёт одного слова, если владелец хочет подписать интерпретацию как разрешение** · причину пустоты в конфиг НЕ выносим (живёт в quirks-доке) · **ре-чек политик стал ПРАВИЛОМ** (триггеры: Google ToS 30.07.2026 · квартал 25.10.2026 · любая правка `accepts_labels`) · дата аккаунта OpenAI — открыта, приоритет низкий (OpenAI вне цепочки). **Продуктовое (Ф3):** Gemini API Additional ToS запрещает клиентов, «likely to be accessed by individuals under 18» — обязательство на конечный продукт, независимо от лейблов · **промпт-слой под лейблом (НОВОЕ, вскрыто вопросом владельца): лейбл сегодня НЕ доезжает до промпта** (чистые данные маршрутизации + load-time гарантия); нужно ли, чтобы переводчик/редактор ЗНАЛИ о свойстве контента — отдельное решение промпт-слоя · ~~мини-каноны сид-дельты~~ ЗАКРЫТО 26.07: все четыре пункта решены, правок сида ноль; книжные канон-решения живут С КНИГОЙ (`books/gu-zhenren/CANON-NOTES.md`), НЕ в D-логе — урок владельца: движок не строится вокруг одной книги · ~~включение платной петли ремонта~~ **РЕШЕНО 26.07 (D39.38): НЕ включается** — замер 0 в её классах; вернуться при расширении классов и ненулевом остатке; **уточнение D39.39: «0» = ноль в охвате детекторов с измеренным теперь recall (латиница 0.50 боевой, битые формы 0.10) — решение в силе (петля видит теми же детекторами), но читать как «финал чист» нельзя** · **вопросы разметки Р1–Р4 (НОВОЕ, D39.39, пакет-6 §5, 21 спорная строка):** гипербола 千万 (довод: тот же автор в гл.2 даёт 数十万 о том же деянии) · что есть дефект в классе 成 — форма или значение (от ответа precision K5c 1.000 или 0.167) · сокращённая форма термина («гу» при dst «гу-тварь») · принципиально неразрешимая офлайн речь/мысль (если да — потолок recall K4b < 1.0 by construction, вписать в контракт правила) · ~~место фикс-пака «чекеры» в очереди~~ **РЕШЕНО 26.07 (D39.40): это ПАК-21, а перед ним мини-прогон ~10 глав на полном бэкенде (после лендинга 20 и 19)**; ответы Р1–Р4 нужны к дизайну пака-21 · ja→ru-реплика §B5 (тайминг) · старые Ф2.5-блокеры (билингв-якорь — D25 п.9 Q1 · контаминация корпуса — D27 п.4 · судья-дублёр D22.6 · планка запуска · publishable/waiver — D25 п.1 · FN-bound L3 — D25 п.4 · юр-пакет · провенанс 12-*-доков · xAI-ключ off перед продом D27 · residual-тачпойнты exp16, не влитые в сид-дельту: мини-голд алиасов · precision@30, `books/gu-zhenren/exp16/`). *Оркестратор:* ~~ратификация дизайна пака-16~~ РЕШЕНО D39.24 (25.07) · фантом-гард D-ссылок в чек-листе лендинга (действует) · ~~doc-sync pass~~ **ИСПОЛНЕН 25.07** (запись ниже: оба puml перерисованы под пост-пак-16 · баннеры на 01/02/03/04/06/07/08/09 · ресёрч-свип 18/18c/19/20/21/22 · README/POLYGON_PACKAGE4 освежены; residual — пинги полигону в его зоне) · ~~приёмка пака-17 фаза 1~~ **РЕШЕНО D39.26 (25.07: принято в редакции §14, три добора приёмки, фаза 2 размечена)** · **`escalation_model` = ОТВЕТИВШАЯ модель (НОВОЕ, вынесено из пака-17): вердикт-изменение** — golden пинит `esc_model` на ОТКАЗАВШЕМ хопе, маскировка не скрывает ⇒ отдельная ратификация с санкционированным пере-капчером · **`--accept-rebill[=usd]` с порогом `min($0.50, 5%×ProjectedBookUSD)` РАТИФИЦИРОВАН D20.2-Q2, но в коде/CLI ОТСУТСТВУЕТ (НОВОЕ)** — живёт только в спеке `backend/docs/D15.2-*.md`; `--resnapshot` пере-пиннит без суммы · **D22.7 (пер-чанковый L3-скрин) — предусловие первой erotica-книги в проде НЕ снято паком-17 (НОВОЕ, держать отдельным гейтом)** · фантом-гард D-ссылок в чек-листе лендинга (действует). *Бэкенд:* **ХВОСТЫ МИНИ-ПРОГОНА (D39.37, 26.07, по убыванию веса):** ~~(1) размеченный набор для чекеров~~ **ЗАКРЫТ 26.07 (полигон-пакет-6, D39.39)** ⇒ породил вход нового фикс-пака «чекеры»: 12 дефектов с file:line (супрессор K5c гасит юнит целиком · атрибуция K4b (23 безатрибутивных + 12 с `!?…`) · `inner_marker` на тире-строки · предохранитель K5a отсекает все реальные магнитуды · заглавные/гомоглифы K2 · 两 в `cheng_re` · U+0301 рвёт `TokenizeCyrillic` · мн.число в `decl.forms`-тулинге · однознаковые ключи пост-чека) — приёмка против набора за $0; **место РЕШЕНО (D39.40): ПАК-21, после мини-прогона на полном бэкенде; + переезд `translitInterjections` в данные (улов владельца)** · (2) **тест-пин джойна WHAT↔WHICH** (мутация оркестратора выжила — фикс-лист №1 следующего касания) · ~~(3) покрытие WHAT↔WHICH + флаговый майнинг-стоп~~ **ПАК-20: фаза 1 ПРИНЯТА 26.07 (D39.41, верификация 39/7/0), решения владельца РАТИФИЦИРОВАНЫ (D39.42: ядро = роль ТЕРМИНОЛОГ, флаг поверх, редактор ест неподписанное с пометкой, банкнота отдельным файлом, точечная ре-редактура по ключу, веб-фетч НЕ в v1); директива фазы 2 вписана в промт пака — ждёт релея сессии; + два архитектурных задела владельца 26.07 КОНТРАКТАМИ, не стройкой (серийный шаринг банка: подписной глоссарий round-trip как сид следующей книги · майнинг из «оригинал + чужой перевод»: target-агностичный вход терминолога, провенанс различает источник, юр-сторона — юр-пакет Ф2.5)** · (4) `dialogue_dash` мерить только на невиданном тексте (подогнан под 蛊真人) · (5) генеральность нарезки: кана/ханьцзи один класс ⇒ ja `est_out` ~1.7× завышен МОЛЧА; `EstimateTokens` недосчитывает кириллицу (резервации занижены) · (6) golden второй фикстурой С langpack · (7) `prompt_override` — честная формулировка гарантии слоя-2 · (8) майнер/банк не-CJK (едет с ja→ru) · (9) `request_log` скоуп/ретеншен + `first_flag_reason` третье представление истории · (10) флор-16000 без модели · (11) `max_tokens`-упоры вернутся на en · ~~пак-17 (фазы 1–2 + дельта)~~ **ЗАКРЫТ ЦЕЛИКОМ 25.07 (D39.26/27/28)** · ~~ПАК-18 «долги контракта»~~ **ПРИНЯТ И ЗАЛЕНДЕН 25.07 (D39.31) — долгов контракта не осталось**; выдавался (`BACKEND_PACK18_DEBTS_SESSION_PROMPT.md`: `--accept-rebill` на снапшотной гранулярности — `guard_hash` в коде нет и не строим · `escalation_model` = авторитетная модель с САНКЦИОНИРОВАННЫМ пере-капчером golden под этот дифф · два нита общности условно-байт-нейтрально) · **ru-target долг (слой 7, `isRuTarget`×11 прод-сайтов · `TokenizeCyrillic`×6 · ключ «ru» в санитайзере) — ЖДЁТ СВОЕГО ДИЗАЙН-ПАКА** (связывает только не-русский таргет, по текущей карте zh/ja/en→ru не срочен) · **ПАК-19 «голос и состояние» ВЫДАН 25.07** (`BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md`, двухфазный; ниты общности едут ЕГО лендингом — D39.31 п.5) · **проекция пере-оплаты в `tmctl status` (НОВОЕ, из отчёта пака-18): спека §9 предлагает заменить булев `ConfigDrift` числом «N чанков, ~$X» — дёшево, `omitempty`, не сделано сознательно** · **не-долги, следующие несущие:** масштаб целой книги (волны/каденс/потолки/ETA на сотнях глав — гоняли максимум 5) · Ф2-механизмы (голос/состояние D21 · native-Gemini судья) · извлечение дискурс-норм/few-shot из ТЕЛ промтов (слой 2, форсинг = ja→ru) · ~~пак-16 ф.2~~ ЗАЛЕНДЕНА 25.07 · per-class исходы ремонта — схемное решение (класс не durable), принимается ВМЕСТЕ с включением петли · ниты общности (владелец 25.07) — **ОТЛОЖЕНЫ ПО ЗАМЕРУ (D39.31 п.5): едут на ближайший ПОЛНЫЙ `--resnapshot`**; для Detail-строк байт-нейтральной формы не существует определительно (дефолт шаблона обязан рендерить текущую строку). Прежняя формулировка: только в байт-нейтральной форме с доказательством исполнением (иначе `CheapGateVersion`/`pack.Version()` двигают снапшот стендовых книг и убивают $0-резюм драфта под замер остатка пака-16): Detail-строки чекеров → шаблоны/цитаты из данных (статический 时辰-текст в generic-файле соврёт второй паре) · множитель ×2 `lintTimeUnits` (`checkers.go:174`) → ключ данных. *Полигон:* ~~пакет-6 (размеченный набор чекеров)~~ **ОТРАБОТАН И ПРИНЯТ 26.07 (D39.39): 3015 позиций, 6 прогонов, $0; metrics.json воспроизведён побайтно; набор на стенде `books/gu-zhenren/labels/` = постоянный измерительный стенд чекеров; **ПАКЕТ-7 (v2, ресёрч-программа): фаза A ИСПОЛНЕНА И ПРИНЯТА 26.07 (D39.43, верификация 24/1/1/0, всё побайтно на пине 996bade); фаза B ЖДЁТ решений владельца Z1–Z4** (рубрика K1–K12 + Q1–Q6 · 修炼 V0–V3 · смета B $1.20–2.60 · $0-добор улик); амендмент D39.42 п.1 — терминолог на полном банке = единицы долларов, не центы (33М вх. токенов); фактура G1–G10 + дефект кластеризации → аддендумом в пак-20** (оставшиеся кандидаты: ёфикатор/санитайзер-классы не размечены — §6.5 отчёта пакета-6; `dialogue_dash` на невиданном тексте) · ~~пакет 5 (факты ToS + 8 пингов)~~ **ОТРАБОТАН ЦЕЛИКОМ И ПРИНЯТ 25.07 (D39.29); все восемь пингов закрыты.** Исторический список закрытых пингов: (1) `00-provider-quirks.md:13,73` — катовер deepseek-chat 24.07 ПРОШЁЛ, пост-катовер факт (жив ли алиас, цены) не внесён — live-проба `/models` + вендор-дока; (2) `00-provider-quirks.md:69` — оговорка «mistral-editor-арм до guard» superseded D39.20 (mistral вон из редакторов; rate-limit-цифры оставить — нужны переводчику канала B пака-17); (3) `09-pilot-protocol.md:3` — ⚠-указатель застрял на D31/glm-5: продлить до пост-D39.22 (dspro-интерим · планка ≤2 не пройдена rerun2 · авто-QA слепыми метками D39.20 · стайл-каноны D39.21), тело не трогать; (4) `15-segmentation-empirics.md:6-7` — шапка до сих пор «НИ ОДНОГО платного вызова», а эксп залендён REV.2 (D39.7/D39.8, $12.79) — перевернуть статус-блок; (5) `16-bank-mining.md:3` — дописать финал «ИСПОЛНЕН И ЗАЛЕНДЁН (D39.10); gender-тачпойнт §7 закрыт (D39.19); продолжение — Go-майнер парити EXACT»; (6) `experiments/README.md` — индекс без строк exp15/16 (сверка застряла на D38.2); (7) `eval/README.md` — не тронут с 19.07 (пре-rerun2): освежить статус под пост-D39.22 (эксп закрыт · судейский риг-стандарт D39.7 · экстракция только `tmctl export`). > - **НАЙДЕНО СПЛОШНЫМ АУДИТОМ 25.07 (D39.34) — 23 позиции, что были ратифицированы, но не попали в трекер; каждая с диспозицией.** *(1) Обязательство, НЕ исполненное:* **флип `Gates.RegressionGuard.Enabled` (D38.4 п.5 «ОБЯЗАН включить» при сборке resnapshot) — ключа нет ни в одном шиппинг-конфиге ⇒ гейт false; rerun2 прошёл без построенной наблюдаемости** → едет обязательной строкой в пак, вызывающий ближайший resnapshot. *(2) Флипы гейтов за подписью владельца (остаются открытыми-с-записью):* coverage `enabled:false` (D12-Q4) · `postcheck_gate` в hard-gate требует пере-замера recall (D24.4+D28.1). *(3) Поглощено курсом:* заморозка «этапа B ~300 глав» (D26 п.1) — **снята D39.33** (масштаб последним), отдельной петлёй не считается. *(4) Открыто, вне «идеала»:* этапы Б+В спеки D15.2 (D33 п.5 «промт выдам после пере-прогона» — прогон был 23.07) — они про content-addressed resume/`guard_hash`, который D39.31 сознательно не строил · F3-остаток идемпотентности · `human_override` LOCK (D29 п.1г, в коде 0 вхождений) · морфо-гейты канцелярита и L1-лемматизация (Ф2, python-сайдкар) · полный OpenCC (0 вхождений) · опциональный пре-перевод гейт. *(5) Мелкие хвосты качества (собрать одним паком когда-нибудь):* ledger-очередь LOW/NOTE из D39.4 · остаток фикс-листа D23 п.4 и D28 п.3(б) · `editor-mono.md` DORMANT (ни в одном конфиге). *(6) Деньги/провайдеры (полигон):* вендор-чек префикс-кэша ZAI/GLM (на нём стоит вся кэш-экономика W1/Q2c) · продление `prices_checked` без правки чисел · scheduler-aware волны под peak-valley DeepSeek (прямой COGS-рычаг) · гейтнутые платные армы (edit-единица >3200 и др.) · wire-probe temperature в thinking-канале DeepSeek. *(7) Продукт/Ф3:* action-security gate перед выдачей tools/webfetch (D25 п.5) · планы research/22 (epub-tag-rewrite, Q7-леджер) · Gemini под-18 обязательство на конечный продукт. *(8) Вход в пак-19:* требование НАРРАТИВНОЙ гендер-аннотации (since_ch-смены + перспектива), а не булева поля — вписано в промт. > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `archive/PROGRESS-2026-07-10-13.md` (D39.6-гигиена) · `archive/PROGRESS-2026-07-13-25.md` (слайс 25.07: стройка паков 11–16, rerun2). Записи ниже — живой хвост. +## Полигон — ПАКЕТ-7 ФАЗА A ИСПОЛНЕНА ($0), СТОП на ратификацию: recall WHICH-канала измерен и он 0.07–0.10, посылка «культивация» уликами не подтверждена, 26.07 + +Отчёт: `archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md`. Код: `eval/pkg7/` (нарезка книг · replace-харнесс боевого майнера с пином на 996bade · майнер каналов en/ja · KWIC · seed-recall · проба измеримости рубрики). Платных вызовов ноль. **Пять измерений на пяти книгах стенда:** (1) recall боевого WHICH-канала против ПОДПИСАННОГО банка 蛊真人 = **0.069–0.103**, по классам name 0.231 / title 0.077–0.154 / place 0.000 / **term 0.040**; 蛊 (8700 вхождений, заглавное понятие) не предлагается никогда — механизм в `emissionEligible` (тип обязан быть name|place|title, длина ≥2 рун); (2) **абляция даёт атрибуцию**: снятие потолка top-200 → 0.429, снятие фильтра типа → 0.875, снятие длины → 0.911, ценой эмиссии 17→15 418 строк при precision спот-чека ≈0.15 ⇒ покрытие ограничивает СЛОЙ ЭМИССИИ, а не детектор, и это количественное основание под D39.42; (3) покрытие не растёт с длиной книги (10гл 15 → 50гл 12), и подписанный на мини-прогоне `学堂家老` исчезает, проглоченный алиас-кластером фамилии `葛家` вместе с родовым `族长` — точечный дефект кластеризации; (4) генеральность: `lang.Load` для ja/en падает громко (D39.15 работает), но под zh-паком en даёт 0 термов и стоп МОЛЧА авто-продолжает, а ja — 8 термов с precision 0 и ключами, которых в книге нет (trad→simp: 範囲→范囲); (5) масштаб квадратичен, `t ≈ chars^2.013`, экстраполяция на приёмочную книгу ≈4.6 ч. Собрано 10 требований к generic-майнеру (хвост №8 D39.37). **Ресёрч:** 6 углов × адверсариальная верификация (84 проверки: 62 CONFIRMED / 14 PARTIAL / 7 MISQUOTED / 1 OVER_ATTRIBUTED / 0 фабрикаций) + критик полноты. Посылка D39.42 п.1 («индустрия — „культивация“») уликами НЕ подтверждена, но и обратное не доказано: издательская сторона стоит на маркетинговых аннотациях, тела изданий не читаны ⇒ владельцу предложен **V0 — не подписывать до одного $0-захода**. Вскрыта ложная сходимость: русские глоссарии сянься = один английский первоисточник + один блогер. **На владельце:** рубрика K1–K12 + Q1–Q6 · строка 修炼 · смета фазы B $1.20–2.60 · нужен ли добор улик перед B. + ## Оркестратор №8 — ПАК-20: фаза 1 принята (D39.41), архитектура фазы 2 решена владельцем (D39.42 — терминолог), 26.07 Фаза-1 сессии закончилась спроектированным СТОПом: синк вскрыл 18 расхождений «доктрина↔код» (4 структурных тупика), корпус доков дал непостроенные ратифицированные ответы (двухрежимная эмиссия §C2-7 · M2 · журнал ревизий), а свои же пять P-решений сессия завалила адверсариальным ревью (9 блокеров). Приёмка воркфлоу (7 верификаторов, 46 клеймов): 39 CONFIRMED / 7 PARTIAL / 0 REFUTED; новая находка приёмки — `bankTokenBudget=144` впритык/отрицателен. Дальше владелец СИНКОМ выправил курс: моя ошибка — его давнее «кто переводит термины, возможно вебфетч, возможно новая роль» я отгородил констрейнтом вместо развилки; его архитектура (терминолог переводит весь смердженный банк по полным контекстам) ратифицирована D39.42 и легла в директиву фазы 2 (промт пака переписан). Направленная банкнота и ось `lane` НЕ строятся; инвариант №2 НЕ трогается (все строки банка фолдятся в версию); поздняя подпись = точечная ре-редактура по ключу со сметой. Полигону добавлен ресёрч-кандидат «веб-фетч терминолога». Урок в копилку: реплика владельца с идеей архитектуры — это вход в развилку, а не шум; «не ратифицировано» — повод принести ратификацию, а не отгородиться. diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 15b09db9..82e8b48f 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1090,3 +1090,13 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу **Уточнения к записанному:** D39.36 п.3 «в инъекцию идёт только CONFIRMED» — верно ТОЛЬКО для редакторского блока; блок переводчика рендерит не-Confirmed с маркером (S1, тест). Моё объяснение владельцу «пометка нужна главам 4–50 того же прогона» было неверным для волновой архитектуры — взято назад (в волне черновики готовы до майнинга; пометка живёт на 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.069–0.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-майнера:** требования G1–G10 (алфавит кандидатов = данные пары · многословные кандидаты · нормализация не выводит строку за пределы книги · порог частоты на класс · разметка исходника = сигнал · контраст на пару · ось эмиссии 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 НЕ стартует** до решений владельца Z1–Z4 (рубрика K1–K12 + Q1–Q6 · вариант по 修炼 · смета B $1.20–2.60 с пре-регистрацией и критериями провала F1–F4 · $0-добор улик перед B). Решения — следующим блоком. diff --git a/docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md b/docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md new file mode 100644 index 00000000..5b2904a5 --- /dev/null +++ b/docs/archive/reports/POLYGON_TERM_RESEARCH_A_2026-07-26.md @@ -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 строки» словаря — +> на деле 36–38, «меньше половины подписываемо» держится. UNVERIFIABLE: «веб-бюджет 200/200» — внутренний +> счётчик той сессии, независимо не проверяется. Решения Z1–Z4 — владельцу; фаза B до них не стартует. + +## Эхо-блок + +``` +1. Задача: терминологическая ресёрч-программа A1–A5 за $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.069–0.103** (три среза), по классам: `name` 0.231 · `title` 0.077–0.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 опознаёт человека в 80–100%). + +### §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 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 /gu25.jsonl +/minerharness/minerharness -in /gu25.jsonl \ + -contrast eval/exp16/data/jieba_dict_general_zh.txt \ + -langpack /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 /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 опознаёт человека в 80–100%**. Литературная рубрика LiTransProQA формулирует терминологический пункт иначе: «Have I maintained consistency in terminology, character names, slang, dialect throughout the text?». Реалистичный потолок человеческого согласия на литературных фреймворках — **Krippendorff α 0.69–0.79**. + +**Согласие и слепота — цифры, а не пожелания.** Landis–Koch: 0.60–0.80 «Substantial», 0.80–1.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.69–0.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-типам и расширен пятью позициями (K8–K12). Колонка «откуда» — чьё это, чтобы не выдавать заимствование за своё. + +**Как читать колонку «шкала».** Машинные критерии (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. + +Ни один балл по K1–K12 не компенсирует катастрофу. Экран бинарный (`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 · 5–8 KWIC-контекстов из исходника (`kwic.py`) · список вариантов dst в **перемешанном** порядке. Что НЕ видит: какой арм/промт произвёл вариант, какой вариант чей, есть ли среди них подписанный. +3. **Способ скоринга — Best-Worst Scaling, а не абсолютные баллы MQM. ⟵ ИЗМЕНЕНО против первого наброска.** Довод — улика, а не вкус: на литературном материале при MQM-оценке ~60% человеческих переводов ошибочно признаются не лучше машинных, тогда как BWS опознаёт человека в 80–100%. Абсолютная MQM-шкала остаётся только для катастроф-экрана (`critical`), где она бинарна и не требует градаций. +4. **Плацебо-арм — СО СЛУЧАЙНОЙ терминологией, а не «без терминологии». ⟵ ИЗМЕНЕНО.** WMT23 показал, что правильная и случайная терминологические подсказки дают ПОХОЖИЙ прирост по метрикам качества; значит арм «без глоссария» ничего не доказывает, а арм «со случайным глоссарием» — доказывает. Дополнительно среди вариантов держим подписанный владельцем dst (где он есть): оценщик, не отличающий подписанный вариант от случайного, отбраковывается вместе со своими вердиктами. +5. **Соль перемешивания — по терму** (не по прогону): урок D13.5, иначе метка утекает через порядок. **Оговорка:** позиционный свап на кураторских наборах, по работе 2026 г., может УХУДШАТЬ результат (−4…−13 п.п.) при доминирующем style bias. Поэтому свап делаем, но эффект свапа измеряем отдельно и не считаем его бесплатным. +6. **Согласие:** считаем **пер-критерий**, а не в среднем — «в среднем сходится» скрывает расхождение на несущем критерии. Каждое мульти-сигнальное утверждение проходит **leave-one-out по оценщику и по критерию**. **Целевой уровень назван заранее:** Krippendorff α 0.69–0.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 до 10–25%, мультиязычный 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, сянься-вебновелла | главы 1–10 / 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, разметка моя, один разметчик): годными выглядят ~4–6 из 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 они уплощены из руби/эмфазы в отдельные короткие строки — **разметка исходника несёт терминологический сигнал, и мы её выбрасываем.** Частоты этих термов **2–10**, то есть они **ниже боевого порога `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.077–0.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| = |A∪B∪C| = 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`; притяжательные `Dior’s` шли отдельным кандидатом; строки капслоком давали ложную улику заглавной (`forever/dead/god` в верхушке); заглавные местоимения клеились к именам (`Miss Steele He`, `Anastasia I’m`); колонтитул 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 живут на частотах 2–10, ниже `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` (`srcdst[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.1–A4.3, и это стоит увидеть ДО подписи: + +| Строка файла | Что говорят улики | +|---|---| +| `修炼 → культивация` (и 修真, 修行 туда же) | развилка §A4.1; в бумаге и академии — «совершенствование»; плюс три разных понятия сведены в одно слово | +| `筑基 → закладка основания` | улик не найдено; площадочный вариант — «Основание Фонда» (явная калька). Строка **UNCLEAR**, а не общепринятая | +| `修士 → культиватор` | та же развилка, что и 修炼 | + +Остальные строки файла (丹田, 灵气, 金丹, 经脉) уликами поддержаны или спорны умеренно. **Это данные для владельца, не правка чужого файла.** + +### A4.5 Японская пара: транскрипция, суффиксы, жанровый лексикон + +**Транскрипция — Поливанов, но статус слабее, чем у Палладия.** «Система Поливанова сейчас является стандартом де-факто, она используется уже многие годы» — то есть **де-факто, а не ГОСТ**. Нормативная рамка «калька с Хепбёрна = ошибка» сформулирована прямо: такие написания «являются следствием незнания русскоязычными авторами традиций транскрипции японских имён и **не допускаются профессиональными лингвистами**». *Оговорка о ложной сходимости (верификатор):* эта формулировка и полемическая цитата про «суси/суши» — **ОДИН голос**: обе сноски РуВики ведут на Вадима Смоленского (susi.ru, 1998 и 1999). Считать их двумя подтверждениями нельзя. Полемику с другой стороны представляет Николай Караев (otaku.ru): «На сегодня поливановская система — это единственная приличная система транслитерации японского языка»; его же фразу с искажённым написанием имени цитировать дословно НЕЛЬЗЯ — четыре независимых чтения страницы дали 2:2 по написанию, сверки не вышло. + +**Позиции ИЗДАТЕЛЬСТВ по транскрипции — UNCLEAR.** Публичных заявлений Истари / Азбуки / Эксмо о системе не найдено. Ближайшее — заявление ПЕРЕВОДЧИКА ранобэ (ник Ushwood; связка «переводчик Истари» держится на одном стороннем источнике, реальное имя **не подтверждено** и в док не ставится): «Вообще-то я при переводе имен с японского обычно руководствуюсь системой Поливанова – **если персонажи являются японцами**». То есть в изданной практике Поливанов — правило по умолчанию **с оговоркой по сеттингу**: для псевдоевропейских миров имена передаются на европейский манер. Для нашей книги это прямо релевантно: `異世界魔術師` — исэкай в псевдоевропейском мире, и имена там `ヤード`, `ソフィ`, `マルガレーテ`, `ロベール` (Ярд, Софи, Маргарете, Робер) — **не японские**. Правило «ja ⇒ Поливанов» без оговорки о сеттинге на этой книге даст неверный результат. + +**Именные суффиксы — единственная улика издательского уровня.** Представитель Истари Комикс Евгений Кольчугин на прямой вопрос: «Японские именные суффиксы есть, да» (личность отвечающего верификатор сверил). Там же — модель принятия решений по терминам, важная для нас: «Если он [переводчик] говорит, что навык надо локализовать, а тут сделать транслитерацию, **мы соглашаемся**» — то есть издательство отдаёт финальное слово по терминологии переводчику, а не единому корпоративному глоссарию. Академическая картина (Туманов, Вестник ТвГУ. Серия «Филология». 2025. № 2 (85). C. 264–270, 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, P1–P4) = 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 предложений, а <15–17 — вообще не аналитическая оценка, а статконтроль. Наши 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.35–0.85** | +| Слепая оценка Best-Worst Scaling: только термы, прошедшие машинный слой (ожидаю ~60%), 3 повтора со свапом | ~700 судейских вызовов | **$0.60–1.30** | +| Резерв на брак и повтор (норма проекта: брак отчитывается, не скрывается) | +30% | **$0.30–0.60** | +| **Итого** | | **$1.20–2.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 между армами на S1–S4 | если P3−P2 ≥ P2−P1, рычаг — рубрика в промте, а не данные | +| **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. **Роль терминолога в коде не проектировалась** — по прямому запрету промта. Требования G1–G10 — фактура, не дизайн. +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** и ответить на Q1–Q4 (веса · K2 против K1 · порог «хорошо» · сокращённая форма и Р3 из D39.39) | §A2.5 | +| **Z2** | По 修炼 — выбрать **V0 / V1 / V2 / V3**. Моя честная рекомендация после правки критиком: **V0 (не подписывать сейчас, сначала закрыть класс улики)**, потому что издательская сторона спора стоит на маркетинговых аннотациях, а не на телах книг. От решения зависят три строки уже созданного `genre-glossary.txt` | §A4.1, §A4.4 | +| **Z3** | Санкционировать **смету фазы B $1.20–2.60** и пре-регистрацию A5 (включая критерий провала F1–F4, снятие 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** | Оркестратору на заметку: **фактура G1–G10 и цифры §A3 — прямой вход в фазу 2 пака-20 и в будущий пак generic-майнера**; отдельно — дефект кластеризации §A3.5, стоивший подписанного терма | §A3 | + +**Заявление = команда.** Приёмка воспроизводима: `build.sh 996bade`, команды §0.2, ожидаемый результат — те же цифры (майнер детерминирован; мои скрипты — тоже, случайная выборка засолена фиксированным сидом 20260726). diff --git a/eval/pkg7/README.md b/eval/pkg7/README.md new file mode 100644 index 00000000..f944b52b --- /dev/null +++ b/eval/pkg7/README.md @@ -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 [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. **Порог частоты и алфавит кандидатов — свойство ЯЗЫКА, не движка.** Японские названия техник живут на частотах 2–10 и в катакане/руби; ханьский n-граммный канал их не видит вовсе. +4. **Правило вершины словосочетания зависит от типа терма** (у имени вершина последняя, у нарицательного — первая). Морфо-чекер без этого правила меряет не то: первая версия `rubric_probe.py` склоняла «Вина» в «Монах Цветочного Вина». +5. **Контраст без опоры вырождается в частоту.** Ранжирование кандзи против словаря другого языка даёт log частоты, а не над-представленность; канал понижен до инвентаря. +6. **Отклонение методики — замерять, а не декларировать.** Нарезка здесь не боевая; свип целевого размера чанка (800/2000/6000) дал Жаккар 1.000 — только после этого цифры можно было предъявлять. diff --git a/eval/pkg7/kwic.py b/eval/pkg7/kwic.py new file mode 100644 index 00000000..c65100ad --- /dev/null +++ b/eval/pkg7/kwic.py @@ -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()) diff --git a/eval/pkg7/mine_nonhan.py b/eval/pkg7/mine_nonhan.py new file mode 100644 index 00000000..70dd00c8 --- /dev/null +++ b/eval/pkg7/mine_nonhan.py @@ -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 I’m», «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", "i’m", "i've", "i’ve", "i'd", "i’d", "i'll", "i’ll", +} + +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()) diff --git a/eval/pkg7/minerharness/build.sh b/eval/pkg7/minerharness/build.sh new file mode 100755 index 00000000..4b8f4dc2 --- /dev/null +++ b/eval/pkg7/minerharness/build.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Собирает minerharness КАК МОДУЛЬ ВНЕ РЕПО (прецедент пакета-6): исходник живёт в eval/ (чтобы был +# ревьюируем и воспроизводим), а go.mod + `replace` материализуются в рабочем каталоге вне git. +# В репозитории второго go.mod не появляется — сборка/vet бэкенда его не видят. +# +# ./build.sh [commit] +# +# work-dir — куда положить модуль и пин-копию бэкенда (скретчпад сессии). +# commit — какой коммит бэкенда пинить (по умолчанию HEAD). Пин обязателен: параллельная +# бэкенд-сессия правит рабочее дерево, и `replace` на него дал бы невоспроизводимый прогон. +set -euo pipefail + +WORK="${1:?usage: build.sh [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)" diff --git a/eval/pkg7/minerharness/main.go b/eval/pkg7/minerharness/main.go new file mode 100644 index 00000000..cc924c37 --- /dev/null +++ b/eval/pkg7/minerharness/main.go @@ -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 ` — параллельно идёт бэкенд-сессия пака-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 [-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) +} diff --git a/eval/pkg7/rubric_probe.py b/eval/pkg7/rubric_probe.py new file mode 100644 index 00000000..cafe1f9b --- /dev/null +++ b/eval/pkg7/rubric_probe.py @@ -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()) diff --git a/eval/pkg7/seed_recall.py b/eval/pkg7/seed_recall.py new file mode 100644 index 00000000..907fee77 --- /dev/null +++ b/eval/pkg7/seed_recall.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Полигон, пакет-7 (A3): recall боевого WHICH-канала против ПОДПИСАННОГО ВЛАДЕЛЬЦЕМ банка. + +Вопрос, на который отвечает скрипт: из терминов, которые владелец УЖЕ подписал для этой книги и +которые физически встречаются в срезе, сколько боевой майнер предлагает сам? + +Дисциплина замера (иначе цифра врёт в свою пользу): + - знаменатель — только те термины сида, что РЕАЛЬНО встречаются в срезе (иначе меряем не recall, + а длину среза); + - майнер прогоняется с ПУСТЫМ сидом, иначе он по построению исключает уже засеянное + (`miner_emit.go:134` — `seedSurfaces[c.Src]` → continue) и recall выйдет 0 по определению; + - засчитывается и попадание в алиас-кластер: движок считает такой кандидат «алиасом + существующего» и владелец видит его на стопе. Обе цифры печатаются раздельно. + - ключи нормализуются тем же способом, что и в движке (trad→simp, 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()) diff --git a/eval/pkg7/split_book.py b/eval/pkg7/split_book.py new file mode 100644 index 00000000..29ddb43a --- /dev/null +++ b/eval/pkg7/split_book.py @@ -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-90-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']*id="([^"]+)"[^>]*href="([^"]+)"', opf)) + ids.update({i: h for h, i in re.findall( + r']*href="([^"]+)"[^>]*id="([^"]+)"', opf)}) + for idref in re.findall(r']*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())