Freeze polygon pre-registration for four falsification arms, the measured deepseek-v4-pro effort-mapping correction, and the premise-review harness proving the probe C rig batches engine-identically
This commit is contained in:
parent
6469479670
commit
4d1d9e525c
18 changed files with 1651 additions and 36 deletions
|
|
@ -53,7 +53,20 @@
|
||||||
| `xhigh` | `high` | `max` |
|
| `xhigh` | `high` | `max` |
|
||||||
| `max` | `max` | `max` |
|
| `max` | `max` | `max` |
|
||||||
|
|
||||||
⇒ (i) **не слать `reasoning_effort` = ехать на `high`** — вот почему боевая форма упирается в стену (п.10); (ii) **на `deepseek-v4-pro` ручка `low` — НЕ РАБОТАЕТ** (маппится в `high`): рычаг бюджета размышления существует ТОЛЬКО у flash, и любой план «перевести роль на pro и придушить эффорт» несостоятелен; (iii) `none` в OpenAI-формате у DeepSeek не существует вовсе (колонка `reasoning.effort:"none"` — Anthropic-формат), запись 04.07 «`none` → 400» в силе.
|
⇒ (i) **не слать `reasoning_effort` = ехать на `high`** — вот почему боевая форма упирается в стену (п.10); (ii) ~~на `deepseek-v4-pro` ручка `low` НЕ РАБОТАЕТ (маппится в `high`); рычаг бюджета размышления существует ТОЛЬКО у flash~~ **ОТОЗВАНО ЗАМЕРОМ 05.08 — см. 3б**; (iii) `none` в OpenAI-формате у DeepSeek не существует вовсе (колонка `reasoning.effort:"none"` — Anthropic-формат), запись 04.07 «`none` → 400» в силе.
|
||||||
|
|
||||||
|
**3б. ⚠ МАППИНГ У `deepseek-v4-pro` СМЕНИЛСЯ — РУЧКА `low` РАБОТАЕТ (полигон, живой замер 2026-08-05).** Исполнение вахты D39.92 (реестр загейченных триггеров, строка 108: «дата > 05.08 ⇒ пере-проба маппинга + сверка changelog»). Триггер сработал по назначению: вендор объявлял смену «early August 2026», и поведение с таблицей 3а больше не сходится.
|
||||||
|
**Метод:** побайтно ОДИН вход (реальный редакторский дифф-вызов), три арма по 3 розыгрыша, `deepseek-v4-pro`, всё `finish=stop`; N=1 непригоден из-за разброса п.2, решает разделение диапазонов. Харнесс — `eval/editor_contract/effort_probe.py`, сырьё `~/books/gu-zhenren/bank-arbitration/eff-*.json`, $0.044163.
|
||||||
|
|
||||||
|
| Арм | `completion_tokens` по розыгрышам | медиана |
|
||||||
|
|---|---|---|
|
||||||
|
| без параметра (вендор-дефолт) | 4183 · 5210 · 5692 | 5210 |
|
||||||
|
| `reasoning_effort:"low"` | 3492 · 1429 · 3589 | **3492** |
|
||||||
|
| `reasoning_effort:"xhigh"` | 10527 · 8666 · 6546 | **8666** |
|
||||||
|
|
||||||
|
**Диапазоны НЕ ПЕРЕСЕКАЮТСЯ:** max(low)=3589 < min(дефолт)=4183 < min(xhigh)=6546. При n=3 идеальное разделение даёт p≈0.05 на пару, и обе пары разделились в предсказанную сторону. ⇒ (а) **`low` на pro срезает размышление примерно на треть — бюджет размышления на pro УПРАВЛЯЕМ**, и вывод «план придушить эффорт на pro несостоятелен» снят; (б) **`xhigh` даёт БОЛЬШЕ дефолта**, то есть это отдельный уровень над `high`, а не `max` — строка таблицы 3а `xhigh`→`max` для pro неверна; вендор-дока, снятая 05.08 живьём, отдаёт `xhigh`→`xhigh` и с замером согласуется.
|
||||||
|
**⚠ Не бесплатно:** п.4 фиксирует, что `low` ПЕРЕ-ВООРУЖАЕТ эхо-мину (на flash 1 чистый китайский выход из 16). **Для pro эхо на `low` НЕ МЕРЕНО НИ РАЗУ** ⇒ «поставить редактору `low`» — обмен цены на риск эха с неизвестным курсом, а не оптимизация. Перед любым таким шагом — эхо-контроль.
|
||||||
|
**Границы:** один вход, одна роль (редактор-дифф), n=3 на арм; `high` и `max` явными значениями не гнались; сноска вендора о смене маппинга на странице ВСЁ ЕЩЁ присутствует, changelog после 31.07 пуст — то есть смена в доке не объявлена, замечена поведением.
|
||||||
4. **⚠ `effort:"low"` ПЕРЕ-ВООРУЖАЕТ эхо-мину:** 1 чистый китайский выход из 16 базовых вызовов (`cjk_share 0.83`, `finish=stop`). **Уточнение рамки D19.2: защита от эха деградирует НЕПРЕРЫВНО с эффортом, а не скачком на «выключено».** ⚠ **Хвост «движок эту ручку слать не может — `echoMineViolation`» ОТОЗВАН (оркестратор 02.08 по коду, D39.86): движок её слать МОЖЕТ.** Гейт (`config/models.go:485-501`) инспектирует МЕХАНИЗМ (`capabilities.reasoning.control` ∈ {`extra_body_disable`, `effort`} + thinking-ключ в `extra_body`), а не значение `stage.reasoning`; у deepseek `control = ReasoningNone`, где `off`/`""` — осознанный no-op, а `low|medium|high` уходят на провод как есть (`llm/capability.go:167-171`). Конфиг `stages[draft].reasoning: "low"` грузится без ошибки и доезжает до провода — проверено исполнением (coldrun-b §3.3). ⇒ **вопрос «включать ли `low`» — экономико-качественный (эхо растёт), а не «нельзя технически»; амендмент гейта для этого НЕ нужен.**
|
4. **⚠ `effort:"low"` ПЕРЕ-ВООРУЖАЕТ эхо-мину:** 1 чистый китайский выход из 16 базовых вызовов (`cjk_share 0.83`, `finish=stop`). **Уточнение рамки D19.2: защита от эха деградирует НЕПРЕРЫВНО с эффортом, а не скачком на «выключено».** ⚠ **Хвост «движок эту ручку слать не может — `echoMineViolation`» ОТОЗВАН (оркестратор 02.08 по коду, D39.86): движок её слать МОЖЕТ.** Гейт (`config/models.go:485-501`) инспектирует МЕХАНИЗМ (`capabilities.reasoning.control` ∈ {`extra_body_disable`, `effort`} + thinking-ключ в `extra_body`), а не значение `stage.reasoning`; у deepseek `control = ReasoningNone`, где `off`/`""` — осознанный no-op, а `low|medium|high` уходят на провод как есть (`llm/capability.go:167-171`). Конфиг `stages[draft].reasoning: "low"` грузится без ошибки и доезжает до провода — проверено исполнением (coldrun-b §3.3). ⇒ **вопрос «включать ли `low`» — экономико-качественный (эхо растёт), а не «нельзя технически»; амендмент гейта для этого НЕ нужен.**
|
||||||
5. **Микро-few-shot: ⚠ ПРЕЖНЯЯ ФОРМУЛИРОВКА ОТОЗВАНА (31.07, адверсариальное ревью).** Числа «reasoning ×1.9» и «постинструкция дешевле на 23.3%» получены НЕПАРНЫМ сравнением средних на распределении, чей разброс ×163 (п.2). **Парный пересчёт по тем же 12 фрагментам:** few-shot думает МЕНЬШЕ базы на **7 из 12** (медиана Δ **−65** ток., знаковый тест p=0.77); постинструкция думает БОЛЬШЕ базы на **9 из 12** (медиана Δ **+208**, p=0.15). «+91%» — артефакт четырёх попаданий в хвост. Скидка постинструкции на 58% состоит из ПРЕФИКСНОГО КЭША (её `system` побайтно равен базовому — sha `e9ea1ae7dae0`, постинструкция уходит в USER-хвост; кэш 95.0% против 27.5% у базы); при пересчёте по некэшированной цене остаётся −9.0%. **Что устояло:** суммарная цена арма few-shot на этой выборке +28.4% и без кэш-эффекта, и **единственный за 48 вызовов выход НА АНГЛИЙСКОМ** (5 770 симв, `finish=stop`) — его. Эхо в том же арме НЕ атрибутируется few-shot (см. п.6).
|
5. **Микро-few-shot: ⚠ ПРЕЖНЯЯ ФОРМУЛИРОВКА ОТОЗВАНА (31.07, адверсариальное ревью).** Числа «reasoning ×1.9» и «постинструкция дешевле на 23.3%» получены НЕПАРНЫМ сравнением средних на распределении, чей разброс ×163 (п.2). **Парный пересчёт по тем же 12 фрагментам:** few-shot думает МЕНЬШЕ базы на **7 из 12** (медиана Δ **−65** ток., знаковый тест p=0.77); постинструкция думает БОЛЬШЕ базы на **9 из 12** (медиана Δ **+208**, p=0.15). «+91%» — артефакт четырёх попаданий в хвост. Скидка постинструкции на 58% состоит из ПРЕФИКСНОГО КЭША (её `system` побайтно равен базовому — sha `e9ea1ae7dae0`, постинструкция уходит в USER-хвост; кэш 95.0% против 27.5% у базы); при пересчёте по некэшированной цене остаётся −9.0%. **Что устояло:** суммарная цена арма few-shot на этой выборке +28.4% и без кэш-эффекта, и **единственный за 48 вызовов выход НА АНГЛИЙСКОМ** (5 770 симв, `finish=stop`) — его. Эхо в том же арме НЕ атрибутируется few-shot (см. п.6).
|
||||||
6. **⚠ ЭХО НА 0731 СТОХАСТИЧНО ПО ВЫЗОВУ, а не детерминировано по фрагменту** — прямая улика: два вызова с **побайтно идентичным запросом** (sha `5c304c9b3cf4`, `effort:low`, cap 8000, temp 0.3) дали `cjk_share 0.000` и `cjk_share 0.831`. **Это отменяет посылку строки ниже («ретраи не помогают — детерминировано для фрагмента»), снятую на `deepseek-chat`, и посылку комментария `disposition.go:138-148` («retry just re-produces them»), из-за которой `cjk_artifact` идёт СРАЗУ в эскалацию.** Арифметика ремонта перевернулась: эскалация на v4-pro = **$0.007335 за ОДНО эхо** ($0.014670 холодного прогона — это ДВА эха на разных чанках, id 58 и 62), простой ре-ген на flash при `effort:low` = **$0.00096** (**≈7.6×** дешевле). Предложение бэкенду: N=1 ре-ген ПЕРЕД эскалацией. Гейт не ослабляется — D19.2 цел.
|
6. **⚠ ЭХО НА 0731 СТОХАСТИЧНО ПО ВЫЗОВУ, а не детерминировано по фрагменту** — прямая улика: два вызова с **побайтно идентичным запросом** (sha `5c304c9b3cf4`, `effort:low`, cap 8000, temp 0.3) дали `cjk_share 0.000` и `cjk_share 0.831`. **Это отменяет посылку строки ниже («ретраи не помогают — детерминировано для фрагмента»), снятую на `deepseek-chat`, и посылку комментария `disposition.go:138-148` («retry just re-produces them»), из-за которой `cjk_artifact` идёт СРАЗУ в эскалацию.** Арифметика ремонта перевернулась: эскалация на v4-pro = **$0.007335 за ОДНО эхо** ($0.014670 холодного прогона — это ДВА эха на разных чанках, id 58 и 62), простой ре-ген на flash при `effort:low` = **$0.00096** (**≈7.6×** дешевле). Предложение бэкенду: N=1 ре-ген ПЕРЕД эскалацией. Гейт не ослабляется — D19.2 цел.
|
||||||
|
|
|
||||||
|
|
@ -126,4 +126,305 @@ F1/F5 — ДОБОР реплик до N=3: у пробы 18 по одному
|
||||||
|
|
||||||
## §2. Результаты
|
## §2. Результаты
|
||||||
|
|
||||||
*(заполняется после прогона)*
|
60 вызовов `deepseek-v4-pro`, все `finish=stop`, **$0.194006 при потолке $0.40**. Реплики ×3 на арм.
|
||||||
|
|
||||||
|
> ## ⛔ РЕТРАКЦИЯ И ПЕРЕ-СЧЁТ (05.08, после воркфлоу-приёмки: 6 слепых линз × скептик на находку)
|
||||||
|
> Приёмка дала **19 подтверждённых дефектов**. Три из них — в моём коде и в моей дисциплине, и они
|
||||||
|
> обесценивали часть первой редакции. Ниже — что сломалось, что пере-считано и что снято.
|
||||||
|
>
|
||||||
|
> ### Р1. КРИТИЧЕСКОЕ: аппликатор молча портил текст
|
||||||
|
> `_fold_map` строил карту офсетов по **NFKC-нормализованному** тексту, а резал **оригинальный**.
|
||||||
|
> Символ, меняющий длину при нормализации («…» → «...»), сдвигал все последующие офсеты:
|
||||||
|
> `"Он ждал… потом ушёл. Небо было серым."` → `"…ушёл. НеНебо было свинцовым."`.
|
||||||
|
> **Испорчено 6 из 36 применённых выходов (всё окно w4)**, при этом риг рапортовал
|
||||||
|
> `applied=ops_total`, `rejected=0` — то есть битые тексты вошли в метрики как успешные.
|
||||||
|
> Самотест 9/9 первой редакции этот класс не содержал.
|
||||||
|
> **Починено** (`apply.py:_fold_chars` — посимвольная нормализация, индекс всегда в ОРИГИНАЛ),
|
||||||
|
> самотест расширен до **15/15** с классом «символы, меняющие длину при NFKC» + удаление + инвариант
|
||||||
|
> (`editor_contract/selftest_apply.py`). Все 36 выходов **пере-применены** из сохранённых ответов
|
||||||
|
> модели — новых вызовов не потребовалось, ответы модели были целы, сломан был только
|
||||||
|
> детерминированный шаг. Текст изменился ровно в 6 файлах.
|
||||||
|
> ⇒ **Клейм «детерминированный apply работает, blast-radius ограничен» в первой редакции был
|
||||||
|
> НЕОБОСНОВАН.** После починки он обоснован, но обоснование принадлежит этой правке, не тому прогону.
|
||||||
|
>
|
||||||
|
> ### Р2. Я ПОДВИНУЛ ЗАФРИЗЕННУЮ ПЛАНКУ (антипаттерн «рационализация задним числом»)
|
||||||
|
> Пре-рег §1.5 требует «фикс-рейт посадок диффа НЕ ХУЖЕ full-regen», знаменатель **12**.
|
||||||
|
> Факт: дифф 11/12, full-regen 12/12. В первой редакции я сузил критерий до «fidelity-first, значит
|
||||||
|
> только класс к2» (знаменатель 6) и записал гейт пройденным.
|
||||||
|
> **По замороженной формулировке гейт НЕ ПРОЙДЕН, и `exp15:147` предписывает: «диффы откладываются
|
||||||
|
> (закрыть D21.4/D21.10 явно)».** Восстанавливаю исходный знаменатель; вывод §2.8 переписан.
|
||||||
|
>
|
||||||
|
> ### Р3. Парсер терял операцию удаления — и моя же поправка была неверна
|
||||||
|
> Первая редакция писала «честный знаменатель 78/81 = 0.963» и «формат не умеет выражать удаление».
|
||||||
|
> Оба неверны: формат удаление выражает штатно, теряет его `OP_RE`. После починки
|
||||||
|
> **D1 = 79/81 = 0.975**, D2 = 74/78 = 0.949. Гейт ≥0.90 проходится, конструктивного пробела
|
||||||
|
> у формата нет — был баг рига.
|
||||||
|
>
|
||||||
|
> ### Р4. Пере-считанные числа (все — после починки)
|
||||||
|
> | Метрика | было напечатано | стало |
|
||||||
|
> |---|---|---|
|
||||||
|
> | комплаенс D1 | 78/80 = 0.975 | **79/81 = 0.975** |
|
||||||
|
> | канон ВНЕ блока, черновик | 18/41 (44%) | **16/41 (39%)** — стемы `фан\s*чжэн` засчитывали слитный дрейф |
|
||||||
|
> | канон ВНЕ блока, D1 · F1 | 18·18·19 · 24·18·20 | **16·16·17 · 22·16·18** |
|
||||||
|
> | цена дифф/full-regen | «1.5–2×» | **кэш-нейтрально ×1.63** (с кэшем ×2.05); по окнам 0.69…3.23, в одном окне дифф ДЕШЕВЛЕ |
|
||||||
|
> | посадки (знаменатель пре-рега) | «к2 паритет ✓» | **11/12 против 12/12 — гейт НЕ пройден** |
|
||||||
|
>
|
||||||
|
> Не изменились: канон В БЛОКЕ (21/21 у обоих ×3), D2 19·20·20 против F5 8·12·6, итог $0.194006.
|
||||||
|
>
|
||||||
|
> ### Р5. Снятые и ослабленные клеймы
|
||||||
|
> - **«Самый крупный эффект… в непокрытой зоне контракт правки решает»** — снято. Числа
|
||||||
|
> (19/20/20 против 8/12/6) верны, но это канон **термов блока при отсутствии блока**, а прямой
|
||||||
|
> замер непокрытой зоны (41 клетка) преимущества диффа НЕ показывает: D1 медиана 16 при черновике 16.
|
||||||
|
> - **«На конверсии 成/分 дифф — единственный, кто работает»** — снято: я считал по окну 1 и молча
|
||||||
|
> выбросил окно 5 (`六成六`=66%), где full-regen чинит 6/6. Направление живёт (19/24 против 8/24).
|
||||||
|
> - **Атом-omission мерен против ЧЕРНОВИКА, а пре-рег §1.4 п.5 требует против предложений ИСХОДНИКА.**
|
||||||
|
> Девиация не была объявлена, и она направленная: дифф по построению не расходится с черновиком там,
|
||||||
|
> где не тронул. Метрика к сравнению контрактов непригодна.
|
||||||
|
> - **Кэш между армами распределён несимметрично** и в первой редакции не упомянут.
|
||||||
|
> - **Проба НЕ является исполнением клетки Q4b дерева `exp15`:** нет якоря A0 и двойного якоря
|
||||||
|
> A0↔A0′, первичная метрика T-inv подменена посадками пробы 18, материал не S2′, суб-фактор
|
||||||
|
> ±сегмент-маркеры не гнался. Это самостоятельная проба по спеке `research/19` §C2, и называть её
|
||||||
|
> «прогоном Q4b» было неверно.
|
||||||
|
> - Из пробы 18: под развязкой конфаунда, которую та проба сама объявила обязательной, вытеснение на
|
||||||
|
> ТРАНСЛЯТОРЕ = 6/6, а не 5/6 ⇒ «цена на редакторе ровно та же» на оси вытеснения неверна
|
||||||
|
> (редактор 5/6, транслятор 6/6). Вывод «редактор не поймает ошибку банка» устоял — он на принятии 6/6.
|
||||||
|
|
||||||
|
### 2.1 Формат-комплаенс — ГЕЙТ ПРОЙДЕН
|
||||||
|
|
||||||
|
| Арм | Вызовов | Операций | Применено | Комплаенс |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **D1** дифф + блок | 18 | 80 | 78 | **0.975** ✓ |
|
||||||
|
| **D2** дифф, посадки | 18 | 78 | 74 | **0.949** ✓ |
|
||||||
|
|
||||||
|
Reject: якорь не найден 4 · неоднозначен 1 · пересечение 1 · пустой 0. **Малформед — 0 из 36.**
|
||||||
|
**Среднее** 4.44 и 4.33 правки на вызов (медианы 4.5 и 4.0 — в первой редакции ярлык «медиана» стоял
|
||||||
|
ошибочно, здесь и в `score.py:78`).
|
||||||
|
|
||||||
|
⇒ **Опасение `research/19` («слабые модели на edit-форматах хуже, чем отдавая текст целиком»;
|
||||||
|
aider: GPT-3.5 46% whole против 30% diff) на `deepseek-v4-pro` и русской прозе НЕ воспроизвелось.**
|
||||||
|
Формат эмитируется надёжно; отброшенные 6 операций из 158 оставили черновик в этих местах нетронутым
|
||||||
|
— blast-radius сработал как задумано.
|
||||||
|
|
||||||
|
⚠ **Две поправки к этому разделу:**
|
||||||
|
1. **Дыра парсера рига.** `ec-D1-w4-r2` содержит 10-й блок с ПУСТОЙ заменой (удаление сноски
|
||||||
|
«— Прим. перев.»); `OP_RE` (`apply.py:15`) требует `\n` перед закрывающим маркером и теряет его
|
||||||
|
молча — операция не попала ни в `ops_total`, ни в малформед. **Честный знаменатель D1 = 78/81 =
|
||||||
|
0.963** (гейт всё ещё пройден). Отдельно: формат anchored search/replace в этой редакции
|
||||||
|
**не умеет выражать удаление** — конструктивный пробел контракта.
|
||||||
|
2. **Толерантность аппликатора не сработала НИ РАЗУ:** из 152 применённых операций **0** доехали
|
||||||
|
благодаря нормализации пробелов/пунктуации — все якоря совпали буквально. Клейм `research/19`
|
||||||
|
«отключение толерантного apply = 9× ошибок» этой пробой не подтверждён и не опровергнут.
|
||||||
|
|
||||||
|
### 2.2 ⚠ NO_CHANGE = 0 из 36 — дисциплина do-nothing НЕ РАБОТАЕТ
|
||||||
|
|
||||||
|
Ни один из 36 дифф-вызовов не ответил «правок нет», хотя промпт объявляет пустой список
|
||||||
|
«полноценным и уважаемым ответом» жирным шрифтом. Модель всегда находит ~4.3 правки.
|
||||||
|
`research/19` §C1 п.1 ставит на это прямо: «Первоклассный выход — „правок нет“ (WMT19 en→ru: ни одна
|
||||||
|
APE-система не побила do-nothing на сильном черновике; LLM-редакторы правят даже корректный вход —
|
||||||
|
гейт снаружи обязателен)». **Замер подтверждает вторую половину цитаты и опровергает надежду на
|
||||||
|
первую: промптом do-nothing не покупается, нужен внешний гейт.**
|
||||||
|
|
||||||
|
### 2.3 Канон
|
||||||
|
|
||||||
|
| | В БЛОКЕ (21 клетка) | ВНЕ блока (41 клетка) |
|
||||||
|
|---|---|---|
|
||||||
|
| черновик | 20/21 | 18/41 |
|
||||||
|
| **D1** дифф+блок | **21/21 · 21/21 · 21/21** | 18 · 18 · 19 (медиана **18**) |
|
||||||
|
| **F1** full-regen+блок | **21/21 · 21/21 · 21/21** | 24 · 18 · 20 (медиана **20**) |
|
||||||
|
| **D2** дифф, БЕЗ блока | **19/21 · 20/21 · 20/21** | 19 · 18 · 18 |
|
||||||
|
| **F5** full-regen, БЕЗ блока | **8/21 · 12/21 · 6/21** | 17 · 21 · 19 |
|
||||||
|
|
||||||
|
> ### ⚠ САМЫЙ КРУПНЫЙ ЭФФЕКТ ПРОБЫ (в первой редакции отчёта эта строка не была посчитана)
|
||||||
|
> **Без блока дифф держит канон 19–20 из 21, full-regen обваливает его до 6–12 из 21 — разница
|
||||||
|
> ВТРОЕ.** Строка `元海` отдельно: дифф 4/6 · 5/6 · 6/6 против full-regen 0/6 · 1/6 · 1/6.
|
||||||
|
> Это ровно та ось, где §0.1 фиксирует дыру: инъекция селективна и на 800 токенов, **56%
|
||||||
|
> подписанных строк живут ВНЕ блока**. В непокрытой зоне контракт правки решает: дифф не трогает
|
||||||
|
> того, чего не просили (blast-radius), full-regen проецирует текст в своё распределение.
|
||||||
|
> Это и есть механизм, ради которого `research/19` предлагал диффы, — и он подтверждён здесь
|
||||||
|
> численно, а не по литературе.
|
||||||
|
|
||||||
|
**Два вывода, оба поправляют пробу 18:**
|
||||||
|
1. **В блоке контракт не различает: оба 21/21 во всех трёх розыгрышах.** Заодно снят вопрос
|
||||||
|
дисперсии — full-regen с блоком стабилен, гуляла именно база БЕЗ блока.
|
||||||
|
2. **Клейм пробы 18 «блок поднимает канон вне блока до 24/41» ОТМЕНЁН:** 24 был удачным одиночным
|
||||||
|
розыгрышем, медиана трёх = 20 при 18 у черновика. Разница внутри шума.
|
||||||
|
Дифф ведёт себя ровно так, как предсказывает конструкция: **держит уровень черновика** (18),
|
||||||
|
потому что не трогает то, чего не просили. Full-regen колеблется 18–24.
|
||||||
|
|
||||||
|
### 2.4 Посаженные дефекты (12 штук, ×3 розыгрыша)
|
||||||
|
|
||||||
|
| Класс | D2 (дифф) | F5 (full-regen) |
|
||||||
|
|---|---|---|
|
||||||
|
| **к2 — смысловое искажение (fidelity)** | **6/6 · 6/6 · 6/6** | **6/6 · 6/6 · 6/6** |
|
||||||
|
| к1 — слово-абракадабра (стиль) | 5/6 · 5/6 · 5/6 | 6/6 · 6/6 · 6/6 |
|
||||||
|
|
||||||
|
**На FIDELITY-классе — паритет 100%.** На классе несуществующих слов дифф стабильно теряет одну:
|
||||||
|
«струмель» выжила 1/3, «дерзновяжной» 2/3 — модель, работающая точечно, реже замечает битую
|
||||||
|
словоформу, чем модель, переписывающая фразу целиком.
|
||||||
|
|
||||||
|
### 2.5 Атом-omission — главное структурное преимущество диффа
|
||||||
|
|
||||||
|
Потеря атомов против черновика (медиана трёх розыгрышей):
|
||||||
|
|
||||||
|
| Арм | числовые атомы | атомы полярности (не/ни/нет/без) |
|
||||||
|
|---|---|---|
|
||||||
|
| **D1** дифф | 1 · 1 · 0 | **1 · 0 · 1** |
|
||||||
|
| **F1** full-regen | 2 · 1 · 2 | **10 · 11 · 7** |
|
||||||
|
| D2 дифф | 1 · 1 · 0 | 0 · 0 · 0 |
|
||||||
|
| F5 full-regen | 0 · 0 · 6 | 2 · 5 · 6 |
|
||||||
|
|
||||||
|
> ### ⚠ КЛЕЙМ «РАЗРЫВ ПО ПОЛЯРНОСТИ ~10×» СНЯТ — ЭТО АРТЕФАКТ МЕТРИКИ
|
||||||
|
> Метрика считала `str.count` по подстрокам `"не "`, `"ни "`, `"нет"`, `"без "`. Проверено
|
||||||
|
> исполнением: **21 из 58 срабатываний в 6 чистых черновиках (36%) — ВНУТРИСЛОВНЫЕ** («мне»,
|
||||||
|
> «камни», «крайне», «жизни», «вполне», «одни»…). Full-regen сливает предложения и выносит именно
|
||||||
|
> эти слова, поэтому получал штраф за переверстку, а не за потерю смысла.
|
||||||
|
> **Пере-счёт токенной метрикой (`\b(не|ни|нет|без)\b`):**
|
||||||
|
>
|
||||||
|
> | Арм | потеря атомов полярности |
|
||||||
|
> |---|---|
|
||||||
|
> | D1 дифф | 1 · 0 · 1 |
|
||||||
|
> | F1 full-regen | 1 · 3 · 1 |
|
||||||
|
> | D2 дифф | 0 · 1 · 0 |
|
||||||
|
> | F5 full-regen | 1 · 0 · 4 |
|
||||||
|
>
|
||||||
|
> Разрыв падает с ~9 до ~1.3 при внутриармовом разбросе F1 = 2 ⇒ **эффект уходит под собственный
|
||||||
|
> шум-пол арма. «Структурно omission-safe» этой пробой НЕ подтверждено и не опровергнуто.**
|
||||||
|
> Числовая метрика загрязнена так же (ложные словоформы «место», «истории», «внутри», «едва»),
|
||||||
|
> и `NUMWORDS` выходит за текст пре-рега §1.4, где сказано «многозначных ЧИСЕЛ» — девиация.
|
||||||
|
|
||||||
|
⚠ **Числовые «потери» диффа — ложные срабатывания:** «десятых» исчезает потому, что дифф ПРАВИЛЬНО
|
||||||
|
приводит доли `成` к процентам, как предписывает `editor.md` (подробнее — §2.6, это выигрыш, а не потеря).
|
||||||
|
|
||||||
|
### 2.6 Конверсия долей 成/分 — fidelity-ось, где выигрывает ДИФФ
|
||||||
|
|
||||||
|
Эталон по исходнику: окно 1 — `八分`=8%, `四成八分`=48%, `三成`=30%; окно 5 — `六成六`=66%.
|
||||||
|
`editor.md` предписывает эту конверсию прямым текстом («доля 成 — десятые: 六成六 = 66%»).
|
||||||
|
|
||||||
|
| Контракт | сайтов починено |
|
||||||
|
|---|---|
|
||||||
|
| **дифф** | 2–3 из 3 в **4 розыгрышах из 6** |
|
||||||
|
| full-regen | **0 из 3** в 3 розыгрышах из 4 |
|
||||||
|
|
||||||
|
⇒ на предписанной промптом конверсии единиц дифф — единственный, кто вообще работает; full-regen
|
||||||
|
её в большинстве розыгрышей не трогает. **В первой редакции отчёта эти правки были поданы как
|
||||||
|
«ложные срабатывания метрики», то есть выигрыш засчитан со знаком минус.**
|
||||||
|
|
||||||
|
**Дефекты, внесённые диффом (глазами), — их ДВА:**
|
||||||
|
- `ec-D1-w1-r2`: `三成` (=30%) → «три процента» (в розыгрыше r1 та же строка починена верно —
|
||||||
|
«тридцать процентов»);
|
||||||
|
- `ec-D2-w1-r3`: «сорок восемь десятых» → «**четыре и восемь десятых**» при эталоне 48% — **хуже
|
||||||
|
черновика**.
|
||||||
|
|
||||||
|
Обвалов длины (форма regression_guard) — 0 из 72 во всех четырёх армах.
|
||||||
|
|
||||||
|
### 2.7 ⚠ ЭКОНОМИКА ПЕРЕВЁРНУТА — несущий результат пробы
|
||||||
|
|
||||||
|
| Арм | медиана выхода | $/вызов |
|
||||||
|
|---|---|---|
|
||||||
|
| **D1** дифф + блок | **3904 ток.** | **$0.004157** |
|
||||||
|
| **F1** full-regen + блок | 1384 ток. | **$0.002686** |
|
||||||
|
| D2 дифф | 4138 ток. | $0.003780 |
|
||||||
|
| F5 full-regen | 1368 ток. | $0.001846 |
|
||||||
|
|
||||||
|
**Дифф-редактор в 1.5–2 раза ДОРОЖЕ full-regen.** Разбор выхода:
|
||||||
|
|
||||||
|
| Арм | completion, медиана | видимый текст | ушло на размышление |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **D1** дифф | 3904 ток. | ≈162 ток. | **3742 ток. (96%)** |
|
||||||
|
| **F1** full-regen | 1449 ток. | ≈612 ток. | 837 ток. (58%) |
|
||||||
|
|
||||||
|
Дифф даёт вчетверо меньше ВИДИМОГО текста — и втрое больше completion, потому что поиск
|
||||||
|
уникального якоря и проверка его однозначности загоняют модель в долгую обдумку, а у DeepSeek
|
||||||
|
reasoning биллится ВНУТРИ `completion_tokens` (`models.yaml`, `reasoning: subset`).
|
||||||
|
|
||||||
|
⇒ **Экономический довод `research/15` («дифф режет output-токены редактора, а это ~90% COGS»)
|
||||||
|
на нашем боевом редакторе НЕ ДЕЙСТВУЕТ.** Он снят на коде и на моделях, где выход = видимый текст;
|
||||||
|
на thinking-модели он инвертируется. Ручки бюджета размышления у `deepseek-v4-pro` нет вовсе
|
||||||
|
(`low`→`high`, quirks 00 §3а), то есть починить это конфигом нельзя — только сменой модели роли.
|
||||||
|
|
||||||
|
### 2.8 Вердикт по пре-регистрированному стоп-гейту
|
||||||
|
|
||||||
|
Гейт дословно: «формат-комплаенс <90% ИЛИ fidelity-first trap хуже full-regen → диффы откладываются;
|
||||||
|
≥90% и атом-omission=0 → дифф-редактор в прод-кандидаты».
|
||||||
|
|
||||||
|
| Условие | Факт | Исход |
|
||||||
|
|---|---|---|
|
||||||
|
| комплаенс ≥0.90 | 0.975 / 0.949 | **✓** |
|
||||||
|
| fidelity-first trap не хуже full-regen | **11/12 против 12/12** (знаменатель ПРЕ-РЕГА) | **✗** |
|
||||||
|
| атом-omission = 0 | метрика признана непригодной (Р5) | **не разрешено** |
|
||||||
|
|
||||||
|
**ВЕРДИКТ ПО ЗАМОРОЖЕННОМУ ГЕЙТУ: диффы ОТКЛАДЫВАЮТСЯ** — `exp15:147` предписывает при этом
|
||||||
|
исходе «закрыть D21.4/D21.10 явно». В первой редакции здесь стояло «формально проходит в
|
||||||
|
прод-кандидаты»; это было получено сужением знаменателя с 12 до 6 ПОСЛЕ вскрытия результата (Р2).
|
||||||
|
|
||||||
|
Отдельно от гейта — что замер всё-таки дал:
|
||||||
|
- контракт **технически состоятелен**: формат эмитируется 0.95–0.98, малформед 0/36, после починки
|
||||||
|
аппликатора применение детерминировано и лосслесс вне спанов;
|
||||||
|
- **дороже**: кэш-нейтрально ×1.63 медианно, но по окнам 0.69…3.23 — в одном окне из шести дифф
|
||||||
|
ДЕШЕВЛЕ, так что «дороже» верно как среднее, а не как свойство каждого вызова;
|
||||||
|
- механизм цены: **96% completion диффа — размышление** (прямая улика: `reasoning_content` медиана
|
||||||
|
10 244 знака у диффа против 2 504 у full-regen, при видимом тексте 486 против 1 835);
|
||||||
|
- ⚠ **вывод «конфигом не чинится» ФАЛЬСИФИЦИРОВАН собственным замером 05.08** (`00-provider-quirks.md`
|
||||||
|
§3б): на `deepseek-v4-pro` ручка `reasoning_effort:"low"` РАБОТАЕТ и срезает размышление примерно
|
||||||
|
на треть (диапазоны трёх армов не пересекаются). Значит у дифф-контракта есть рычаг цены, которого
|
||||||
|
первая редакция его лишила на основании устаревшей строки квирков. Цена этого рычага не бесплатна:
|
||||||
|
квирки §4 фиксируют, что `low` пере-вооружает эхо-мину, и для pro это не мерено;
|
||||||
|
- **do-nothing промптом не покупается** (NO_CHANGE 0/36) — но тестировался промпт со СТИЛЬ-мандатом
|
||||||
|
(51 из 80 операций D1 — чистая лексика), а цитата `research/19` §C1 относится к fidelity-only
|
||||||
|
проходу, который не гнался.
|
||||||
|
|
||||||
|
**Что это значит для «как правильно пользоваться редактором» — по данным, не по вкусу:**
|
||||||
|
1. **Дифф-контракт технически состоятелен** на нашей модели: формат эмитируется 0.95–0.98,
|
||||||
|
детерминированный apply работает, blast-radius ограничен, омиссия структурно не течёт.
|
||||||
|
2. **Но окупается он только там, где выход ≠ размышление.** На thinking-редакторе с несъёмным
|
||||||
|
`high` он покупает omission-safety за +50–100% цены. Это торг, а не улучшение.
|
||||||
|
3. **Оба контракта одинаково исполняют закон банка (21/21)** ⇒ вопрос диффов НЕ про терминологию.
|
||||||
|
Терминология решается покрытием инъекции: 18/41 канона вне блока — у обеих ролей, в черновике.
|
||||||
|
4. **Гейт снаружи обязателен в любом контракте:** LLM-редактор правит даже там, где править нечего
|
||||||
|
(NO_CHANGE 0/36).
|
||||||
|
|
||||||
|
### 2.9 «Заявление = команда»
|
||||||
|
|
||||||
|
| Число | Команда |
|
||||||
|
|---|---|
|
||||||
|
| 41 клетка вне блока, 18/41 в черновике, армы пробы 18 | `editor_contract/offblock.py` |
|
||||||
|
| аппликатор 9/9 | инлайн-самотест (в журнале сессии) |
|
||||||
|
| смета до запуска | `editor_contract/probe.py --plan` |
|
||||||
|
| комплаенс · канон · посадки · omission · длина · деньги | `editor_contract/score.py` |
|
||||||
|
| разбор completion на размышление/текст, систематический промах к1, ложность числовой omission, `三成`-дефект | инлайн-скрипты по сырью `ec-*.json` |
|
||||||
|
|
||||||
|
### 2.10 Границы
|
||||||
|
|
||||||
|
Одна книга, одна пара, 6 окон по ~500 ру-токенов выхода (боевой потолок единицы 3200), одна модель.
|
||||||
|
Reflow-проход (Q4c) не гонялся — проза и переверстка НЕ сравнивались, дифф-арм их по спеке не делает.
|
||||||
|
Судейского рига нет: ни одного клейма о художественном качестве. Метрика полярности штрафует
|
||||||
|
легитимную переверстку (§2.5а). Числа `research/15` про «90% COGS» относятся к доле редактора в
|
||||||
|
общем счёте прогона и этой пробой не пере-мерены — опровергнут только вывод «дифф её режет».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3. ПРЕ-РЕГИСТРАЦИЯ ЧЕТЫРЁХ ФАЛЬСИФИКАЦИЙ (санкция владельца 05.08; записано ДО платных вызовов)
|
||||||
|
|
||||||
|
Каждый арм построен как попытка **убить** вывод §2, а не подтвердить его. Потолок **$0.50**,
|
||||||
|
живой стоп $0.44 + проекционный гард. Харнесс `eval/editor_contract/final_probes.py`, префикс `fp-*`.
|
||||||
|
|
||||||
|
| Арм | Какой вывод убивает | Дизайн | Вызовов |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **L** | «дифф дороже» — артефакт несъёмного thinking? | тот же материал §2, оба контракта, `reasoning_effort:"low"` (ручка признана рабочей замером 05.08, quirks §3б) | 36 |
|
||||||
|
| **G** | «комплаенс 0.95–0.98» снят на окнах ВПЯТЕРО короче боевых? | единица **~3200 ру-ток. выхода** = боевой `EditCeilingOut` (фактически 3201 и 3215), где уникальность якоря обязана деградировать | 12 + 2 черновика |
|
||||||
|
| **E** | «дифф дороже» — артефакт РУССКОЙ фертильности? | тот же дизайн на **английской цели** (для zh→en r в проекте не мерен вовсе) | 24 + 6 черновиков |
|
||||||
|
| **N** | «do-nothing не покупается промптом»? | **fidelity-only** дифф-промпт без стиль-мандата — там, где `research/19` §C1 и ставит NO_CHANGE | 18 |
|
||||||
|
|
||||||
|
**Критерии, фиксируются здесь:**
|
||||||
|
- **L** — вывод §2.7 падает, если на `low` отношение цены дифф/full-regen опускается **≤1.0** по медиане
|
||||||
|
окон; держится, если остаётся >1.0. Обязательный **эхо-контроль**: доля ханьцзы в выходе (quirks §4
|
||||||
|
фиксирует, что `low` пере-вооружает эхо-мину на flash; на pro НЕ МЕРЕНО НИ РАЗУ) — ненулевое эхо
|
||||||
|
делает «поставить low» непригодным независимо от цены.
|
||||||
|
- **G** — вывод §2.1 падает, если комплаенс на боевой единице **<0.90** (порог `exp15:147`).
|
||||||
|
Ожидаемое направление — вниз: якорь тем менее уникален, чем длиннее текст.
|
||||||
|
- **E** — вывод §2.7 падает, если на английской цели отношение цены **≤1.0**. Скорится ТОЛЬКО формат,
|
||||||
|
цена, объём выхода и доля размышления: **en-голда в проекте нет**, канон-формы взяты из
|
||||||
|
`research/24` §F1 + фан-канон RI, поэтому точность перевода этим армом НЕ измеряется.
|
||||||
|
- **N** — вывод §2.2 падает, если доля `NO_CHANGE` на fidelity-only промпте **>0** (в §2 было 0/36).
|
||||||
|
|
||||||
|
Средние исходы печатаются как «неопределённо», не абсорбируются. Девиации объявляются.
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
# ⚠ ФИКС-2 (адверсариальная приёмка 05.08): прежняя форма требовала `\n` ПЕРЕД закрывающим маркером
|
||||||
|
# и потому МОЛЧА теряла операцию с ПУСТОЙ заменой (удаление фрагмента) — она не попадала ни в
|
||||||
|
# ops_total, ни в malformed. Формат удаление выражает штатно; терял его парсер.
|
||||||
OP_RE = re.compile(
|
OP_RE = re.compile(
|
||||||
r"<{5,9}\s*SEARCH\s*\n(?P<search>.*?)\n={5,9}\s*\n(?P<replace>.*?)\n>{5,9}\s*REPLACE",
|
r"<{5,9}[ \t]*SEARCH[ \t]*\n(?P<search>.*?)\n={5,9}[ \t]*\n(?P<replace>.*?)\n?>{5,9}[ \t]*REPLACE",
|
||||||
re.S)
|
re.S)
|
||||||
NO_CHANGE = "NO_CHANGE"
|
NO_CHANGE = "NO_CHANGE"
|
||||||
|
|
||||||
|
|
@ -26,40 +29,46 @@ PUNCT_CLASSES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _fold_chars(text: str):
|
||||||
|
"""Посимвольная свёртка ОРИГИНАЛЬНОГО текста → [(символ_свёртки, индекс_в_ОРИГИНАЛЕ)].
|
||||||
|
|
||||||
|
⚠ ФИКС-1 (адверсариальная приёмка 05.08, критический). Прежняя версия строила карту офсетов по
|
||||||
|
`unicodedata.normalize("NFKC", text)`, а РЕЗАЛА оригинал: символ, меняющий длину при
|
||||||
|
нормализации («…» → «...»), сдвигал все последующие офсеты, и apply вставлял замену не туда,
|
||||||
|
молча склеивая текст («Он ждал… потом ушёл. НеНебо было серым»). Поймано на 6 из 36 выходов
|
||||||
|
(окно w4), при этом риг рапортовал applied=ops_total и rejected=0.
|
||||||
|
Лечение: нормализация ПОСИМВОЛЬНАЯ, индекс всегда указывает в ОРИГИНАЛ; расширение одного
|
||||||
|
символа в несколько даёт несколько записей с ОДНИМ и тем же исходным индексом.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
prev_space = True # схлоп ведущих пробелов
|
||||||
|
for i, ch in enumerate(text):
|
||||||
|
if ch.isspace():
|
||||||
|
if not prev_space:
|
||||||
|
out.append((" ", i))
|
||||||
|
prev_space = True
|
||||||
|
continue
|
||||||
|
prev_space = False
|
||||||
|
norm_ch = unicodedata.normalize("NFKC", ch)
|
||||||
|
for chars, to in PUNCT_CLASSES:
|
||||||
|
if ch in chars:
|
||||||
|
norm_ch = to
|
||||||
|
break
|
||||||
|
for c in norm_ch:
|
||||||
|
out.append((c, i))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def fold(s: str) -> str:
|
def fold(s: str) -> str:
|
||||||
"""Нормализация ДЛЯ ПОИСКА (не для вывода): NFKC · схлоп пробелов · классы пунктуации."""
|
"""Нормализация ДЛЯ ПОИСКА (не для вывода). Тот же путь, что у _fold_map, — иначе ключ и стог
|
||||||
s = unicodedata.normalize("NFKC", s)
|
могли бы свернуться по-разному (полнострочный NFKC компонует то, чего посимвольный не делает)."""
|
||||||
for chars, to in PUNCT_CLASSES:
|
return "".join(c for c, _ in _fold_chars(s)).strip()
|
||||||
for ch in chars:
|
|
||||||
s = s.replace(ch, to)
|
|
||||||
return re.sub(r"\s+", " ", s).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _fold_map(text: str) -> tuple[str, list[int]]:
|
def _fold_map(text: str) -> tuple[str, list[int]]:
|
||||||
"""Свёрнутый текст + карта «индекс в свёрнутом → индекс в оригинале»."""
|
"""Свёрнутый текст + карта «индекс в свёрнутом → индекс в ОРИГИНАЛЕ»."""
|
||||||
out, idx = [], []
|
pairs = _fold_chars(text)
|
||||||
prev_space = True # схлоп ведущих пробелов
|
return "".join(c for c, _ in pairs), [i for _, i in pairs]
|
||||||
for i, ch in enumerate(unicodedata.normalize("NFKC", text)):
|
|
||||||
if ch.isspace():
|
|
||||||
if prev_space:
|
|
||||||
continue
|
|
||||||
out.append(" ")
|
|
||||||
idx.append(i)
|
|
||||||
prev_space = True
|
|
||||||
continue
|
|
||||||
prev_space = False
|
|
||||||
for chars, to in PUNCT_CLASSES:
|
|
||||||
if ch in chars:
|
|
||||||
ch = to
|
|
||||||
break
|
|
||||||
if len(ch) == 1:
|
|
||||||
out.append(ch)
|
|
||||||
idx.append(i)
|
|
||||||
else: # … → ...
|
|
||||||
for c in ch:
|
|
||||||
out.append(c)
|
|
||||||
idx.append(i)
|
|
||||||
return "".join(out), idx
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
95
eval/editor_contract/effort_probe.py
Normal file
95
eval/editor_contract/effort_probe.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Пере-проба маппинга эффорта `deepseek-v4-pro` — исполнение вахты D39.92 (реестр загейченных
|
||||||
|
триггеров, строка 108): «дата > 05.08 ⇒ пере-проба маппинга + сверка changelog; абсорбция в
|
||||||
|
00-provider-quirks.md — полигоном».
|
||||||
|
|
||||||
|
ЧТО ПРОВЕРЯЕТ (поведением, не докой):
|
||||||
|
(1) квирк 3а «на v4-pro ручка `low` НЕ РАБОТАЕТ — маппится в high» — у нас стоит СО СЛОВ вендора;
|
||||||
|
(2) расхождение реестра с первоисточником: `00-provider-quirks.md` §3а пишет для pro
|
||||||
|
`xhigh` → `max`, а вендор-дока (снята 05.08) отдаёт `xhigh` → `xhigh`.
|
||||||
|
|
||||||
|
Метод: побайтно ОДИН вход (реальный редакторский дифф-вызов окна 0), три арма — без параметра
|
||||||
|
(вендор-дефолт) · `low` · `xhigh` — по 3 розыгрыша. N=1 непригоден: квирки 00 §2 документируют
|
||||||
|
разброс reasoning ×163 на побайтно одном входе. Решает СРАВНЕНИЕ РАСПРЕДЕЛЕНИЙ reasoning_tokens.
|
||||||
|
Сырьё durable, префикс `eff-*`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import statistics as st
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
|
||||||
|
sys.path.insert(0, str(HERE))
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||||||
|
|
||||||
|
import editor_wire_probe as P # noqa: E402
|
||||||
|
import probe as Q # noqa: E402
|
||||||
|
from inject_probe import pick_windows # noqa: E402
|
||||||
|
|
||||||
|
REPS = 3
|
||||||
|
ARMS = {"default": None, "low": "low", "xhigh": "xhigh"}
|
||||||
|
CEILING_USD = 0.10
|
||||||
|
|
||||||
|
|
||||||
|
def build_msgs():
|
||||||
|
_, buf, terms = pick_windows()[0]
|
||||||
|
draft = P.draft_of(0)
|
||||||
|
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
|
||||||
|
return Q.messages("diff", buf, draft, blk)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
msgs = build_msgs()
|
||||||
|
cl = P.client()
|
||||||
|
total = 0.0
|
||||||
|
for arm, eff in ARMS.items():
|
||||||
|
for r in range(1, REPS + 1):
|
||||||
|
tag = f"eff-{arm}-r{r}"
|
||||||
|
if (RAW / f"{tag}.json").exists():
|
||||||
|
print(f"[{tag}] уже есть — пропуск")
|
||||||
|
continue
|
||||||
|
if total > CEILING_USD:
|
||||||
|
print(f"СТОП: ${total:.4f} > ${CEILING_USD}")
|
||||||
|
return
|
||||||
|
extra = {"reasoning_effort": eff} if eff else None
|
||||||
|
try:
|
||||||
|
rec = P.call(cl, msgs, tag, extra=extra)
|
||||||
|
total += rec["cost_usd"]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{tag}] ОШИБКА {type(e).__name__}: {str(e)[:200]}")
|
||||||
|
time.sleep(0.4)
|
||||||
|
print(f"TOTAL пере-пробы эффорта: ${total:.6f}")
|
||||||
|
|
||||||
|
|
||||||
|
def score():
|
||||||
|
print("вход побайтно ОДИН во всех армах; reasoning_tokens = поле usage провайдера\n")
|
||||||
|
base = None
|
||||||
|
for arm in ARMS:
|
||||||
|
rows = []
|
||||||
|
for r in range(1, REPS + 1):
|
||||||
|
f = RAW / f"eff-{arm}-r{r}.json"
|
||||||
|
if not f.exists():
|
||||||
|
continue
|
||||||
|
d = json.load(open(f, encoding="utf-8"))
|
||||||
|
rows.append((d["completion_tokens"], d["reasoning_chars"], d["cost_usd"], d["finish"]))
|
||||||
|
if not rows:
|
||||||
|
print(f" {arm}: нет данных (все вызовы упали?)")
|
||||||
|
continue
|
||||||
|
rc = [b for _, b, _, _ in rows]
|
||||||
|
ct = [a for a, _, _, _ in rows]
|
||||||
|
med = st.median(rc)
|
||||||
|
if arm == "default":
|
||||||
|
base = med
|
||||||
|
delta = "" if base is None or arm == "default" else f" ⇒ против дефолта ×{med/base:.2f}" if base else ""
|
||||||
|
print(f" {arm:8s} n={len(rows)} · reasoning медиана {med:.0f} симв (розыгрыши {rc}) · "
|
||||||
|
f"completion {ct} · finish={ {x[3] for x in rows} }{delta}")
|
||||||
|
print("\nЧитать так: если `low` НЕ отличается от дефолта — квирк 3а «ручка мёртвая» подтверждён")
|
||||||
|
print("ПОВЕДЕНИЕМ. Если отличается — квирк устарел, и бюджет размышления на pro управляем.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
(score if "--score" in sys.argv else run)()
|
||||||
253
eval/editor_contract/final_probes.py
Normal file
253
eval/editor_contract/final_probes.py
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ЧЕТЫРЕ ФАЛЬСИФИКАЦИИ выводов пробы 19 (санкция владельца 05.08).
|
||||||
|
|
||||||
|
Каждый арм построен как попытка УБИТЬ мой собственный заголовок, а не подтвердить его.
|
||||||
|
|
||||||
|
L — «дифф дороже» это артефакт несъёмного thinking? → тот же v4-pro, но reasoning_effort:"low"
|
||||||
|
(ручка признана рабочей замером 05.08, quirks §3б). + ЭХО-КОНТРОЛЬ: `low` пере-вооружает
|
||||||
|
эхо-мину (quirks §4), на pro не мерено ни разу.
|
||||||
|
G — «комплаенс 0.95-0.98» снят на окнах ВПЯТЕРО короче боевых? → единица ~3200 ру-ток. выхода
|
||||||
|
(боевой потолок EditCeilingOut, chunker.go), где уникальность якоря обязана деградировать.
|
||||||
|
E — «дифф дороже» это артефакт РУССКОЙ фертильности (r=1.65-1.90 zh→ru, exp01; для zh→en
|
||||||
|
замера в проекте НЕТ)? → тот же дизайн на английской цели.
|
||||||
|
N — «do-nothing промптом не покупается» мерено на промпте со СТИЛЬ-мандатом, а research/19 §C1
|
||||||
|
ставит NO_CHANGE на FIDELITY-ONLY проход → узкий промпт без стиля.
|
||||||
|
|
||||||
|
Сырьё durable, префиксы fp-L/G/E/N. Материал L и N — те же 6 окон и черновики пробы 19.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
|
||||||
|
SRC = Path.home() / "books" / "gu-zhenren" / "coldrun-a" / "guzhenren-ch1-10.gb18030.txt"
|
||||||
|
sys.path.insert(0, str(HERE))
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||||||
|
|
||||||
|
import editor_wire_probe as P # noqa: E402
|
||||||
|
import probe as Q # noqa: E402
|
||||||
|
from apply import apply_ops # noqa: E402
|
||||||
|
from inject_probe import pick_windows, TERMS, render_translator, block_for # noqa: E402
|
||||||
|
|
||||||
|
CEILING_USD = 0.50
|
||||||
|
HARD_STOP = 0.44
|
||||||
|
REPS = 3
|
||||||
|
FERT_CJK, FERT_OTHER = 1.1978, 0.3852 # chunker.go SegBudget
|
||||||
|
EDIT_CEILING_OUT = 3200 # боевой потолок единицы редактуры
|
||||||
|
|
||||||
|
# Английские канон-формы для блока. НЕ голд: en-голда в проекте нет. Взяты из research/24 §F1
|
||||||
|
# (там свежая en-редакция дала «Aperture · Gu Master · primeval stone»), остальные — фан-канон RI.
|
||||||
|
# ⇒ арм E скорится ТОЛЬКО по формату/цене/объёму, НЕ по точности перевода.
|
||||||
|
EN_FORMS = {"空窍": "Aperture", "真元": "primeval essence", "元石": "primeval stone",
|
||||||
|
"元海": "sea of primeval essence", "蛊师": "Gu Master", "族长": "clan leader",
|
||||||
|
"月光蛊": "Moonlight Gu", "春秋蝉": "Spring Autumn Cicada"}
|
||||||
|
EN_HEADER = ("CANONICAL RENDERINGS of names and terms (in the draft, the source term on the left MUST "
|
||||||
|
"be rendered exactly by the form on the right — bring any divergence to it, inflecting by "
|
||||||
|
"context; do not introduce other variants and do not change anything else):")
|
||||||
|
|
||||||
|
EN_VARS = dict(P.RENDER_VARS, target_lang="en", audience="adult webnovel readers",
|
||||||
|
genre="webnovel", transcription="pinyin")
|
||||||
|
|
||||||
|
|
||||||
|
def est_out(t: str) -> float:
|
||||||
|
cjk = other = 0
|
||||||
|
for ch in t:
|
||||||
|
if ch.isspace():
|
||||||
|
continue
|
||||||
|
if unicodedata.category(ch) == "Lo" and ord(ch) > 0x2E80:
|
||||||
|
cjk += 1
|
||||||
|
else:
|
||||||
|
other += 1
|
||||||
|
return FERT_CJK * cjk + FERT_OTHER * other
|
||||||
|
|
||||||
|
|
||||||
|
def render_from(path: Path, text: str, draft: str, vars_=None) -> tuple[str, str]:
|
||||||
|
canon = P.strip_comments(path.read_text(encoding="utf-8"))
|
||||||
|
sys_part, user = canon.split(P.USER_SEP, 1)
|
||||||
|
old = P.RENDER_VARS
|
||||||
|
if vars_:
|
||||||
|
P.RENDER_VARS = vars_
|
||||||
|
try:
|
||||||
|
return P.render(sys_part.strip(), text, draft), P.render(user.strip(), text, draft)
|
||||||
|
finally:
|
||||||
|
P.RENDER_VARS = old
|
||||||
|
|
||||||
|
|
||||||
|
def msgs_from(path: Path, text: str, draft: str, block: str | None, vars_=None):
|
||||||
|
s, u = render_from(path, text, draft, vars_)
|
||||||
|
m = [{"role": "system", "content": s}]
|
||||||
|
if block:
|
||||||
|
m.append({"role": "system", "content": block})
|
||||||
|
m.append({"role": "user", "content": u})
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def en_block(terms: list[str]) -> str:
|
||||||
|
return EN_HEADER + "\n" + "\n".join(f"- {t} → «{EN_FORMS[t]}»" for t in terms if t in EN_FORMS)
|
||||||
|
|
||||||
|
|
||||||
|
def long_units(n=2):
|
||||||
|
"""Смежные абзацы источника до боевого потолка единицы (~3200 ру-токенов выхода)."""
|
||||||
|
paras = [p for p in SRC.read_text(encoding="gb18030").split("\n") if p.strip()]
|
||||||
|
out, i = [], 60
|
||||||
|
while len(out) < n and i < len(paras) - 40:
|
||||||
|
buf, j = "", i
|
||||||
|
while j < len(paras) and est_out(buf) < EDIT_CEILING_OUT:
|
||||||
|
buf += paras[j] + "\n"
|
||||||
|
j += 1
|
||||||
|
terms = [t for t in TERMS if t in buf]
|
||||||
|
if len(terms) >= 3:
|
||||||
|
out.append((i, buf, terms))
|
||||||
|
i = j + 5
|
||||||
|
else:
|
||||||
|
i += 5
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --- планы армов -------------------------------------------------------------------------------
|
||||||
|
def plan_L():
|
||||||
|
"""L: тот же материал пробы 19, но reasoning_effort:'low' на обоих контрактах."""
|
||||||
|
out = []
|
||||||
|
for rep in range(1, REPS + 1):
|
||||||
|
for k, (_, buf, terms) in enumerate(pick_windows()):
|
||||||
|
d = P.draft_of(k)
|
||||||
|
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
|
||||||
|
out.append((f"fp-LD-w{k}-r{rep}", Q.messages("diff", buf, d, blk), "diff", d, {"reasoning_effort": "low"}))
|
||||||
|
out.append((f"fp-LF-w{k}-r{rep}", Q.messages("full", buf, d, blk), "full", d, {"reasoning_effort": "low"}))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def plan_N():
|
||||||
|
"""N: fidelity-only дифф-промпт — там, где research/19 §C1 ставит NO_CHANGE."""
|
||||||
|
pr = HERE / "prompts" / "editor-diff-fidelity.md"
|
||||||
|
out = []
|
||||||
|
for rep in range(1, REPS + 1):
|
||||||
|
for k, (_, buf, terms) in enumerate(pick_windows()):
|
||||||
|
d = P.draft_of(k)
|
||||||
|
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
|
||||||
|
out.append((f"fp-N-w{k}-r{rep}", msgs_from(pr, buf, d, blk), "diff", d, None))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def plan_G(drafts: dict):
|
||||||
|
"""G: боевая единица ~3200 ру-токенов выхода."""
|
||||||
|
out = []
|
||||||
|
for rep in range(1, REPS + 1):
|
||||||
|
for u, (idx, buf, terms) in enumerate(long_units()):
|
||||||
|
d = drafts.get(f"G{u}")
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
|
||||||
|
out.append((f"fp-GD-u{u}-r{rep}", Q.messages("diff", buf, d, blk), "diff", d, None))
|
||||||
|
out.append((f"fp-GF-u{u}-r{rep}", Q.messages("full", buf, d, blk), "full", d, None))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def plan_E(drafts: dict):
|
||||||
|
"""E: английская цель, тот же дизайн."""
|
||||||
|
pd = HERE / "prompts" / "editor-diff-en.md"
|
||||||
|
pf = HERE / "prompts" / "editor-en.md"
|
||||||
|
out = []
|
||||||
|
for rep in (1, 2):
|
||||||
|
for k, (_, buf, terms) in enumerate(pick_windows()):
|
||||||
|
d = drafts.get(f"E{k}")
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
blk = en_block(terms)
|
||||||
|
out.append((f"fp-ED-w{k}-r{rep}", msgs_from(pd, buf, d, blk, EN_VARS), "diff", d, None))
|
||||||
|
out.append((f"fp-EF-w{k}-r{rep}", msgs_from(pf, buf, d, blk, EN_VARS), "full", d, None))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def make_drafts():
|
||||||
|
"""Черновики, которых нет в сырье: 2 длинные единицы (ru) и 6 английских окон."""
|
||||||
|
cl = P.client()
|
||||||
|
drafts, total = {}, 0.0
|
||||||
|
for u, (idx, buf, terms) in enumerate(long_units()):
|
||||||
|
tag = f"fp-draft-G{u}"
|
||||||
|
f = RAW / f"{tag}.json"
|
||||||
|
if not f.exists():
|
||||||
|
system, user = render_translator(buf)
|
||||||
|
m = [{"role": "system", "content": system},
|
||||||
|
{"role": "system", "content": block_for(terms, None, False)},
|
||||||
|
{"role": "user", "content": user}]
|
||||||
|
total += P.call(cl, m, tag, model="deepseek-v4-flash", temperature=0, max_tokens=16000,
|
||||||
|
extra={"reasoning_effort": "low"}, prices=(0.14, 0.0028, 0.28))["cost_usd"]
|
||||||
|
drafts[f"G{u}"] = json.load(open(RAW / f"{tag}.json", encoding="utf-8"))["content"]
|
||||||
|
# ⚠ НЕ боевой zh-ru/translator.md: он параметризован лишь в строке 1, а в 9/10/14 русский зашит
|
||||||
|
# прозой — прогон поверх него дал РУССКИЕ черновики fp-draft-E*. Зеркало для en — в моей зоне.
|
||||||
|
tpl = HERE / "prompts" / "translator-en.md"
|
||||||
|
for k, (_, buf, terms) in enumerate(pick_windows()):
|
||||||
|
tag = f"fp-draft-E{k}"
|
||||||
|
f = RAW / f"{tag}.json"
|
||||||
|
if not f.exists():
|
||||||
|
s, u = render_from(tpl, buf, "", EN_VARS)
|
||||||
|
blk = ("GLOSSARY (use these approved renderings of names and terms consistently):\n"
|
||||||
|
+ "\n".join(f"{t} → {EN_FORMS[t]}" for t in terms if t in EN_FORMS))
|
||||||
|
m = [{"role": "system", "content": s}, {"role": "system", "content": blk},
|
||||||
|
{"role": "user", "content": u}]
|
||||||
|
total += P.call(cl, m, tag, model="deepseek-v4-flash", temperature=0, max_tokens=16000,
|
||||||
|
extra={"reasoning_effort": "low"}, prices=(0.14, 0.0028, 0.28))["cost_usd"]
|
||||||
|
drafts[f"E{k}"] = json.load(open(RAW / f"{tag}.json", encoding="utf-8"))["content"]
|
||||||
|
print(f"черновики готовы, доплата ${total:.6f}")
|
||||||
|
return drafts
|
||||||
|
|
||||||
|
|
||||||
|
def spent() -> float:
|
||||||
|
return sum(json.load(open(f, encoding="utf-8"))["cost_usd"]
|
||||||
|
for f in RAW.glob("fp-*.json") if not f.name.endswith(".applied.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_run(only):
|
||||||
|
drafts = make_drafts()
|
||||||
|
plan = []
|
||||||
|
if not only or "L" in only:
|
||||||
|
plan += plan_L()
|
||||||
|
if not only or "N" in only:
|
||||||
|
plan += plan_N()
|
||||||
|
if not only or "G" in only:
|
||||||
|
plan += plan_G(drafts)
|
||||||
|
if not only or "E" in only:
|
||||||
|
plan += plan_E(drafts)
|
||||||
|
cl = P.client()
|
||||||
|
total = spent()
|
||||||
|
print(f"план: {len(plan)} вызовов; уже потрачено ${total:.6f}")
|
||||||
|
worst = 16000 / 1e6 * P.PRICE_OUT + 6000 / 1e6 * P.PRICE_IN
|
||||||
|
for tag, msgs, kind, draft, extra in plan:
|
||||||
|
if (RAW / f"{tag}.json").exists():
|
||||||
|
continue
|
||||||
|
if total + worst > CEILING_USD or total >= HARD_STOP:
|
||||||
|
print(f"СТОП (проекционный гард): ${total:.4f} + худший ${worst:.4f} > ${CEILING_USD}")
|
||||||
|
return
|
||||||
|
rec = P.call(cl, msgs, tag, extra=extra)
|
||||||
|
total += rec["cost_usd"]
|
||||||
|
if kind == "diff":
|
||||||
|
r = apply_ops(draft, rec["content"])
|
||||||
|
(RAW / f"{tag}.applied.json").write_text(json.dumps(dict(
|
||||||
|
tag=tag, ops_total=r.ops_total, applied=r.applied, no_change=r.no_change,
|
||||||
|
malformed=r.malformed, rejected_notfound=r.rejected_notfound,
|
||||||
|
rejected_ambiguous=r.rejected_ambiguous, rejected_overlap=r.rejected_overlap,
|
||||||
|
rejected_empty=r.rejected_empty, details=r.details, text=r.text),
|
||||||
|
ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
|
print(f" apply: ops={r.ops_total} applied={r.applied} no_change={r.no_change} malformed={r.malformed}")
|
||||||
|
time.sleep(0.3)
|
||||||
|
print(f"TOTAL финальных проб: ${total:.6f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--run", nargs="*", default=None, help="L G E N (пусто = все)")
|
||||||
|
a = ap.parse_args()
|
||||||
|
if a.run is not None:
|
||||||
|
cmd_run(a.run)
|
||||||
|
else:
|
||||||
|
for u, (i, buf, terms) in enumerate(long_units()):
|
||||||
|
print(f"длинная единица {u}: @para{i} est_out={est_out(buf):.0f} ру-ток, термов {len(terms)}")
|
||||||
|
print("армы: L(36) N(18) G(12+2черн) E(24+6черн) = 98 вызовов")
|
||||||
|
|
@ -28,10 +28,10 @@ GOLD = {r["bank_src"]: r["gold_dst"]
|
||||||
# Стем-регексы канонических форм. КОНСЕРВАТИВНО: скорим только строки, у которых форма
|
# Стем-регексы канонических форм. КОНСЕРВАТИВНО: скорим только строки, у которых форма
|
||||||
# опознаётся устойчивым стемом. Отброшенные строки печатаются — знаменатель честный.
|
# опознаётся устойчивым стемом. Отброшенные строки печатаются — знаменатель честный.
|
||||||
STEM = {
|
STEM = {
|
||||||
"方源": r"фан\s*юан",
|
"方源": r"фан\s+юан",
|
||||||
"方正": r"фан\s*чжэн",
|
"方正": r"фан\s+чжэн",
|
||||||
"古月": r"гуюэ", # «Гу Юэ» раздельно НЕ матчится — это и есть дрейф
|
"古月": r"гуюэ", # «Гу Юэ» раздельно НЕ матчится — это и есть дрейф
|
||||||
"古月漠北": r"гуюэ\s*мобэй",
|
"古月漠北": r"гуюэ\s+мобэй",
|
||||||
"蛊虫": r"гу-черв",
|
"蛊虫": r"гу-черв",
|
||||||
"本命蛊": r"гу\s*жизни",
|
"本命蛊": r"гу\s*жизни",
|
||||||
"希望蛊": r"гу\s*надежды",
|
"希望蛊": r"гу\s*надежды",
|
||||||
|
|
|
||||||
35
eval/editor_contract/prompts/editor-diff-en.md
Normal file
35
eval/editor_contract/prompts/editor-diff-en.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
You are a literary editor of a fiction translation into English. You are given the SOURCE text and its draft translation.
|
||||||
|
Genre: {{genre}}. Audience: {{audience}}. Book: «{{title}}».
|
||||||
|
|
||||||
|
Your mandate is TARGETED editing. You do NOT rewrite the text: you emit a LIST OF REPLACEMENT OPERATIONS, each fixing one concrete place.
|
||||||
|
|
||||||
|
What to fix, in order of importance:
|
||||||
|
- FIDELITY: distortions against the source (language «{{source_lang}}») — inverted polarity, wrong numbers and ranks, lost or added meaning atoms, misread realia, calqued gestures and idioms.
|
||||||
|
- TERMS: proper names, terms and realia STRICTLY per the supplied glossary (if attached); bring any divergence to the canonical form, do not invent other renderings.
|
||||||
|
- STYLE: calques, officialese, unnatural constructions, broken word forms. BUT a repetition standing in parallel positions of the source is an authorial refrain: keep it.
|
||||||
|
|
||||||
|
Do NOT: re-paragraph, merge or split sentences for structure; "improve" places that are already correct and readable. **An empty list of edits is a full and respected answer.** Do not touch what was not asked.
|
||||||
|
|
||||||
|
RESPONSE FORMAT (strict):
|
||||||
|
|
||||||
|
If there are no edits, output exactly one line:
|
||||||
|
NO_CHANGE
|
||||||
|
|
||||||
|
Otherwise output one or more operations, each exactly in this form:
|
||||||
|
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
verbatim fragment of the DRAFT to replace
|
||||||
|
=======
|
||||||
|
replacement text
|
||||||
|
>>>>>>> REPLACE
|
||||||
|
|
||||||
|
Rules (violation = the operation is discarded): SEARCH must match the draft character for character; SEARCH must be UNIQUE in the draft (extend with neighbouring words until unique); take the minimal sufficient fragment, not a whole paragraph; no line numbers, no ellipses inside SEARCH; operations must not overlap; nothing except operations (or NO_CHANGE) may appear in the response.
|
||||||
|
|
||||||
|
---USER---
|
||||||
|
Source text (language «{{source_lang}}»):
|
||||||
|
|
||||||
|
{{text}}
|
||||||
|
|
||||||
|
Draft translation to edit:
|
||||||
|
|
||||||
|
{{draft}}
|
||||||
39
eval/editor_contract/prompts/editor-diff-fidelity.md
Normal file
39
eval/editor_contract/prompts/editor-diff-fidelity.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
Ты — редактор-корректор смысла художественного перевода на русский язык. Тебе даны ИСХОДНЫЙ текст и его черновой перевод.
|
||||||
|
Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}».
|
||||||
|
|
||||||
|
Твой мандат УЗКИЙ: только ВЕРНОСТЬ смыслу против исходника (язык «{{source_lang}}»). Стиль, лексику, благозвучие, вёрстку НЕ ТРОГАЙ ВООБЩЕ.
|
||||||
|
|
||||||
|
Правь ТОЛЬКО это:
|
||||||
|
- перевёрнутая полярность (утверждение вместо отрицания и наоборот);
|
||||||
|
- неверные числа, доли, ранги, количества (доля 成 — десятые: 六成六 = 66%; 时辰 = 2 часа);
|
||||||
|
- потерянные или добавленные смысловые атомы (участники, действия, объекты);
|
||||||
|
- неверно понятые реалии и буквальные кальки жестов/идиом, искажающие смысл;
|
||||||
|
- имена собственные и термины, расходящиеся с приведённым глоссарием (если он приложен).
|
||||||
|
|
||||||
|
Категорически НЕ правь: синонимы, порядок слов, канцелярит, «неуклюжие» обороты, длину предложений, абзацы, пунктуацию — если смысл при этом верен.
|
||||||
|
|
||||||
|
**Если смысловых ошибок нет — так и ответь. Пустой список правок это ПРАВИЛЬНЫЙ и ожидаемый ответ на хороший черновик.**
|
||||||
|
|
||||||
|
ФОРМАТ ОТВЕТА (строго):
|
||||||
|
|
||||||
|
Если смысловых правок нет — выведи ровно одну строку:
|
||||||
|
NO_CHANGE
|
||||||
|
|
||||||
|
Иначе — одну или несколько операций строго такой формы:
|
||||||
|
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
дословный фрагмент ЧЕРНОВИКА
|
||||||
|
=======
|
||||||
|
текст замены
|
||||||
|
>>>>>>> REPLACE
|
||||||
|
|
||||||
|
Правила: SEARCH копируется из черновика символ в символ и должен быть УНИКАЛЕН (расширь соседними словами, если нужно); бери минимальный достаточный фрагмент; никаких номеров строк и многоточий внутри SEARCH; операции не пересекаются; ничего, кроме операций или NO_CHANGE, в ответе быть не должно.
|
||||||
|
|
||||||
|
---USER---
|
||||||
|
Исходный текст (язык «{{source_lang}}»):
|
||||||
|
|
||||||
|
{{text}}
|
||||||
|
|
||||||
|
Черновик перевода для редактуры:
|
||||||
|
|
||||||
|
{{draft}}
|
||||||
21
eval/editor_contract/prompts/editor-en.md
Normal file
21
eval/editor_contract/prompts/editor-en.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
You are a literary editor of a fiction translation into English. You are given the SOURCE text and its draft translation.
|
||||||
|
Genre: {{genre}}. Audience: {{audience}}. Book: «{{title}}».
|
||||||
|
|
||||||
|
Your mandate is literary editing of the draft against the source:
|
||||||
|
- Check the draft's meaning against the source (language «{{source_lang}}»): fix distortions of meaning, misread realia, calqued gestures and idioms.
|
||||||
|
- Fix style: remove calques from «{{source_lang}}», officialese, unnatural constructions; write living literary English. BUT a repetition standing in parallel positions of the source is an authorial refrain: keep it.
|
||||||
|
- Render proper names, terms and realia STRICTLY per the supplied glossary (if attached): bring any divergence to the canonical form; do not invent other renderings.
|
||||||
|
- Layout: assemble the narrative into natural English paragraphs, NOT copying the source's line-by-line breaks. Start each new line of direct speech on a new paragraph.
|
||||||
|
- Preserve the draft's COMPLETENESS: do not drop or add sentences; do not re-invent scenes.
|
||||||
|
- Honorifics: {{honorifics}}. Foreignization/domestication balance: {{venuti}}.
|
||||||
|
|
||||||
|
Output ONLY the edited translation — no preambles, comments, notes or markdown headings.
|
||||||
|
|
||||||
|
---USER---
|
||||||
|
Source text (language «{{source_lang}}»):
|
||||||
|
|
||||||
|
{{text}}
|
||||||
|
|
||||||
|
Draft translation to edit:
|
||||||
|
|
||||||
|
{{draft}}
|
||||||
40
eval/editor_contract/prompts/translator-en.md
Normal file
40
eval/editor_contract/prompts/translator-en.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!-- ПОЛИГОННЫЙ промпт, НЕ прод. Зеркало backend/prompts/zh-ru/translator.md для АНГЛИЙСКОЙ цели.
|
||||||
|
|
||||||
|
Зачем отдельный файл (ревью посылки 05.08, находка): боевой `zh-ru/translator.md` параметризован
|
||||||
|
только в СТРОКЕ 1 (`{{target_lang}}`), а в строках 9/10/14 русский зашит ПРОЗОЙ — «по нормам
|
||||||
|
русского языка», «Пиши живым литературным русским языком», «опираясь на существующие русские
|
||||||
|
переводы». Прогон арма E поверх него дал черновики fp-draft-E0..E3 на РУССКОМ (E0: 1499 знаков
|
||||||
|
кириллицы против 22 латиницы) — арм измерял бы русскую фертильность под видом английской.
|
||||||
|
|
||||||
|
Структура зеркалит оригинал 1:1 (шапка · translation brief · единицы и приёмы · формат вывода ·
|
||||||
|
---USER---), меняются только те места, где оригинал зашивает язык. Язык инструкций = язык цели,
|
||||||
|
как и в оригинале. Формы терминов приходят отдельным системным блоком, как в проде.
|
||||||
|
|
||||||
|
⚠ Это НЕ кандидат в прод и не en-langpack: арм E скорится по формату/цене/объёму, а НЕ по
|
||||||
|
точности перевода (en-голда в проекте нет). -->
|
||||||
|
You are a professional literary translator from «{{source_lang}}» into «{{target_lang}}».
|
||||||
|
Book genre: {{genre}}. Audience: {{audience}}. Book: «{{title}}».
|
||||||
|
|
||||||
|
Translation brief:
|
||||||
|
- Honorifics: {{honorifics}}. Transcription of names and realia: {{transcription}}.
|
||||||
|
- Foreignization/domestication balance (0 — full adaptation, 1 — keep the foreign): {{venuti}}.
|
||||||
|
- Footnotes: {{footnotes}}.
|
||||||
|
- Translate EVERY sentence of the source: omit nothing, abridge nothing, add nothing of your own.
|
||||||
|
- Lay the text out by English typographic norms, do NOT copy the source's line breaks: set direct
|
||||||
|
speech as its own paragraph in double quotation marks.
|
||||||
|
- Write in living literary English; avoid officialese and calques from the source language.
|
||||||
|
|
||||||
|
Units and devices (common to zh→en):
|
||||||
|
- Convert MEASURES into units the reader knows: the Chinese 时辰 = 2 hours (三个时辰 ≈ six hours,
|
||||||
|
半个时辰 ≈ one hour); do not transliterate the term itself. The fraction 成 means tenths
|
||||||
|
(六成 = 60%, 六成六 = 66%), NOT the decimal «6.6».
|
||||||
|
- Render POEMS and titles of classics BY MEANING (not by transliteration), leaning on existing
|
||||||
|
English translations.
|
||||||
|
- Take the form of proper names and terms from the attached glossary (if one is present).
|
||||||
|
|
||||||
|
Output ONLY the translation, with no service preambles, comments, explanations or markdown headings.
|
||||||
|
|
||||||
|
---USER---
|
||||||
|
Translate the following fragment:
|
||||||
|
|
||||||
|
{{text}}
|
||||||
217
eval/editor_contract/score.py
Normal file
217
eval/editor_contract/score.py
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Скоринг арма Q4b по пре-регу §1.4–1.5 (`docs/experiments/19-editor-contract-q4b.md`).
|
||||||
|
Все метрики детерминированные, $0. Судейского рига нет и он вне скоупа.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import statistics as st
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||||
|
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(HERE))
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||||||
|
|
||||||
|
import editor_wire_probe as P # noqa: E402
|
||||||
|
from inject_probe import pick_windows # noqa: E402
|
||||||
|
from offblock import offblock_cells, norm # noqa: E402
|
||||||
|
|
||||||
|
WINS = pick_windows()
|
||||||
|
REPS = (1, 2, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def out_text(tag: str) -> str | None:
|
||||||
|
"""Финальный текст арма: для диффа — ПРИМЕНЁННЫЙ, для full-regen — content."""
|
||||||
|
ap = RAW / f"{tag}.applied.json"
|
||||||
|
if ap.exists():
|
||||||
|
return json.load(open(ap, encoding="utf-8"))["text"]
|
||||||
|
f = RAW / f"{tag}.json"
|
||||||
|
if not f.exists():
|
||||||
|
return None
|
||||||
|
return json.load(open(f, encoding="utf-8"))["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def arm_tags(arm: str, reps=REPS) -> dict[int, list[str]]:
|
||||||
|
"""{окно: [теги реплик]}, существующие. A1/A5 пробы 18 = реплика r1 армов F1/F5."""
|
||||||
|
legacy = {"F1": "edp-A1-w{k}", "F5": "edp-A5-w{k}"}
|
||||||
|
out = {}
|
||||||
|
for k in range(6):
|
||||||
|
tags = []
|
||||||
|
if arm in legacy and (RAW / (legacy[arm].format(k=k) + ".json")).exists():
|
||||||
|
tags.append(legacy[arm].format(k=k))
|
||||||
|
for r in reps:
|
||||||
|
t = f"ec-{arm}-w{k}-r{r}"
|
||||||
|
if (RAW / f"{t}.json").exists():
|
||||||
|
tags.append(t)
|
||||||
|
if tags:
|
||||||
|
out[k] = tags
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --- 1. формат-комплаенс -------------------------------------------------------------------
|
||||||
|
def compliance():
|
||||||
|
print("=== 1. ФОРМАТ-КОМПЛАЕНС (стоп-гейт Q4b: ≥0.90) ===")
|
||||||
|
for arm in ("D1", "D2"):
|
||||||
|
ops = ap = nc = mal = 0
|
||||||
|
rej = dict(notfound=0, ambiguous=0, overlap=0, empty=0)
|
||||||
|
n = 0
|
||||||
|
for k, tags in arm_tags(arm).items():
|
||||||
|
for t in tags:
|
||||||
|
f = RAW / f"{t}.applied.json"
|
||||||
|
if not f.exists():
|
||||||
|
continue
|
||||||
|
d = json.load(open(f, encoding="utf-8"))
|
||||||
|
n += 1
|
||||||
|
ops += d["ops_total"]; ap += d["applied"]
|
||||||
|
nc += d["no_change"]; mal += d["malformed"]
|
||||||
|
for key in rej:
|
||||||
|
rej[key] += d[f"rejected_{key}"]
|
||||||
|
c = ap / ops if ops else 1.0
|
||||||
|
print(f" {arm}: вызовов {n} · операций {ops} · применено {ap} · "
|
||||||
|
f"КОМПЛАЕНС {c:.3f} {'✓' if c >= 0.90 else '✗'}")
|
||||||
|
print(f" reject: не найден {rej['notfound']} · неоднозначен {rej['ambiguous']} · "
|
||||||
|
f"пересечение {rej['overlap']} · пустой {rej['empty']} | "
|
||||||
|
f"NO_CHANGE {nc} · малформед {mal}")
|
||||||
|
print(f" правок на вызов: медиана {ops/n:.1f}" if n else "")
|
||||||
|
|
||||||
|
|
||||||
|
# --- 2/3. канон в блоке и вне блока --------------------------------------------------------
|
||||||
|
def canon_inblock():
|
||||||
|
print("\n=== 2. КАНОН В БЛОКЕ (21 клетка; знаменатель пробы 18) ===")
|
||||||
|
base = {}
|
||||||
|
for k, (_, buf, terms) in enumerate(WINS):
|
||||||
|
d = norm(P.draft_of(k))
|
||||||
|
base[k] = [t for t in terms if t != "元海"]
|
||||||
|
dn = sum(1 for k in base for t in base[k] if P.hits(norm(P.draft_of(k)), t)[0])
|
||||||
|
print(f" черновик: {dn}/21")
|
||||||
|
for arm in ("D1", "F1"):
|
||||||
|
per = []
|
||||||
|
for k, tags in arm_tags(arm).items():
|
||||||
|
pass
|
||||||
|
rows = {}
|
||||||
|
for k in range(6):
|
||||||
|
for i, t in enumerate(arm_tags(arm).get(k, [])):
|
||||||
|
o = out_text(t)
|
||||||
|
if o is None:
|
||||||
|
continue
|
||||||
|
rows.setdefault(i, [0, 0])
|
||||||
|
n = base[k]
|
||||||
|
rows[i][0] += sum(1 for x in n if P.hits(norm(o), x)[0])
|
||||||
|
rows[i][1] += len(n)
|
||||||
|
vals = [f"{v[0]}/{v[1]}" for v in rows.values()]
|
||||||
|
nums = [v[0] for v in rows.values()]
|
||||||
|
print(f" {arm}: розыгрыши {' · '.join(vals)} медиана {st.median(nums):.0f}/21" if nums else f" {arm}: нет данных")
|
||||||
|
|
||||||
|
|
||||||
|
def canon_offblock():
|
||||||
|
print("\n=== 3. КАНОН ВНЕ БЛОКА (41 клетка; §0.1) ===")
|
||||||
|
cells = offblock_cells()
|
||||||
|
dn = sum(1 for k, s, d, rx in cells
|
||||||
|
if re.search(rx, norm(P.draft_of(k))))
|
||||||
|
print(f" черновик: {dn}/{len(cells)}")
|
||||||
|
for arm in ("D1", "F1", "D2", "F5"):
|
||||||
|
rows = {}
|
||||||
|
for k, s, dst, rx in cells:
|
||||||
|
for i, t in enumerate(arm_tags(arm).get(k, [])):
|
||||||
|
o = out_text(t)
|
||||||
|
if o is None:
|
||||||
|
continue
|
||||||
|
rows.setdefault(i, [0, 0])
|
||||||
|
rows[i][1] += 1
|
||||||
|
rows[i][0] += bool(re.search(rx, norm(o)))
|
||||||
|
nums = [v[0] for v in rows.values()]
|
||||||
|
if nums:
|
||||||
|
print(f" {arm}: розыгрыши {' · '.join(f'{v[0]}/{v[1]}' for v in rows.values())}"
|
||||||
|
f" медиана {st.median(nums):.0f}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- 4. фикс-рейт посадок ------------------------------------------------------------------
|
||||||
|
def plants():
|
||||||
|
print("\n=== 4. ФИКС-РЕЙТ 12 ПОСАДОК (пре-рег: дифф НЕ ХУЖЕ full-regen) ===")
|
||||||
|
for arm in ("D2", "F5"):
|
||||||
|
rows = {}
|
||||||
|
for k in range(6):
|
||||||
|
for i, t in enumerate(arm_tags(arm).get(k, [])):
|
||||||
|
o = out_text(t)
|
||||||
|
if o is None:
|
||||||
|
continue
|
||||||
|
n = norm(o)
|
||||||
|
rows.setdefault(i, {"k1": [0, 0], "k2": [0, 0]})
|
||||||
|
for cls, old, new, bad_rx, fix_rx in P.DEFECTS[k]:
|
||||||
|
rows[i][cls][1] += 1
|
||||||
|
rows[i][cls][0] += (not re.search(bad_rx, n))
|
||||||
|
if rows:
|
||||||
|
for cls in ("k1", "k2"):
|
||||||
|
v = [f"{r[cls][0]}/{r[cls][1]}" for r in rows.values()]
|
||||||
|
print(f" {arm} {cls}: розыгрыши {' · '.join(v)}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- 5. атом-omission ----------------------------------------------------------------------
|
||||||
|
NUMWORDS = ("два", "две", "три", "четыр", "пят", "шест", "сем", "восем", "девят", "десят",
|
||||||
|
"двадцат", "тридцат", "сорок", "пятьдесят", "сто", "тысяч", "пятьсот", "полов")
|
||||||
|
NEG = ("не ", "ни ", "нет", "без ")
|
||||||
|
|
||||||
|
|
||||||
|
def omission():
|
||||||
|
print("\n=== 5. АТОМ-OMISSION (числа + полярность; пре-рег: у диффа должно быть 0) ===")
|
||||||
|
for arm in ("D1", "F1", "D2", "F5"):
|
||||||
|
rows = {}
|
||||||
|
for k in range(6):
|
||||||
|
draft = P.draft_of(k) if arm in ("D1", "F1") else P.planted_draft(k, P.draft_of(k))[0]
|
||||||
|
dnums = set(re.findall(r"\d{2,}", draft)) | {w for w in NUMWORDS if w in draft.lower()}
|
||||||
|
dneg = sum(draft.lower().count(x) for x in NEG)
|
||||||
|
for i, t in enumerate(arm_tags(arm).get(k, [])):
|
||||||
|
o = out_text(t)
|
||||||
|
if o is None:
|
||||||
|
continue
|
||||||
|
onums = set(re.findall(r"\d{2,}", o)) | {w for w in NUMWORDS if w in o.lower()}
|
||||||
|
oneg = sum(o.lower().count(x) for x in NEG)
|
||||||
|
rows.setdefault(i, [0, 0, 0])
|
||||||
|
rows[i][0] += len(dnums - onums) # исчезнувшие числовые атомы
|
||||||
|
rows[i][1] += max(0, dneg - oneg) # просевшая полярность
|
||||||
|
rows[i][2] += 1
|
||||||
|
if rows:
|
||||||
|
v = [f"чисел-{r[0]}/полярн-{r[1]}" for r in rows.values()]
|
||||||
|
print(f" {arm}: розыгрыши {' · '.join(v)}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- 6. внесённые дефекты + 7. деньги ------------------------------------------------------
|
||||||
|
def guards_and_money():
|
||||||
|
print("\n=== 6. ВНЕСЁННЫЕ ДЕФЕКТЫ (форма regression_guard: обвал длины >40%) ===")
|
||||||
|
for arm in ("D1", "F1", "D2", "F5"):
|
||||||
|
drops = tot = 0
|
||||||
|
for k in range(6):
|
||||||
|
draft = P.draft_of(k) if arm in ("D1", "F1") else P.planted_draft(k, P.draft_of(k))[0]
|
||||||
|
dl = len(re.sub(r"\s", "", draft))
|
||||||
|
for t in arm_tags(arm).get(k, []):
|
||||||
|
o = out_text(t)
|
||||||
|
if o is None:
|
||||||
|
continue
|
||||||
|
tot += 1
|
||||||
|
if len(re.sub(r"\s", "", o)) < dl * 0.60:
|
||||||
|
drops += 1
|
||||||
|
if tot:
|
||||||
|
print(f" {arm}: обвалов длины {drops}/{tot}")
|
||||||
|
print("\n=== 7. ДЕНЬГИ И ВЫХОДНЫЕ ТОКЕНЫ (COGS-ось) ===")
|
||||||
|
for arm in ("D1", "D2", "F1", "F5"):
|
||||||
|
outs, cost, n = [], 0.0, 0
|
||||||
|
for k in range(6):
|
||||||
|
for t in arm_tags(arm).get(k, []):
|
||||||
|
f = RAW / f"{t}.json"
|
||||||
|
if not f.exists():
|
||||||
|
continue
|
||||||
|
r = json.load(open(f, encoding="utf-8"))
|
||||||
|
outs.append(r["completion_tokens"]); cost += r["cost_usd"]; n += 1
|
||||||
|
if n:
|
||||||
|
print(f" {arm}: вызовов {n} · выход медиана {st.median(outs):.0f} ток. "
|
||||||
|
f"(мин {min(outs)} макс {max(outs)}) · ${cost:.6f} · ${cost/n:.6f}/вызов")
|
||||||
|
tot = sum(json.load(open(f, encoding="utf-8"))["cost_usd"] for f in RAW.glob("ec-*.json")
|
||||||
|
if not f.name.endswith(".applied.json"))
|
||||||
|
print(f" ИТОГО проба Q4b: ${tot:.6f} (потолок $0.40)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
compliance(); canon_inblock(); canon_offblock(); plants(); omission(); guards_and_money()
|
||||||
97
eval/editor_contract/selftest_apply.py
Normal file
97
eval/editor_contract/selftest_apply.py
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Самотест аппликатора. Расширен после адверсариальной приёмки 05.08, которая нашла два дефекта,
|
||||||
|
пропущенных первой редакцией теста (9/9 «зелёных»):
|
||||||
|
ФИКС-1 — карта офсетов строилась по NFKC-нормализованному тексту, а резался оригинал: символ,
|
||||||
|
меняющий длину при нормализации («…»), сдвигал все последующие офсеты → тихая порча.
|
||||||
|
ФИКС-2 — операция с ПУСТОЙ заменой (удаление) молча терялась парсером.
|
||||||
|
Добавлены: класс «символы, меняющие длину при NFKC», удаление, и ИНВАРИАНТ — текст вне заменённых
|
||||||
|
спанов обязан совпадать с черновиком.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from apply import apply_ops, fold # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def op(s, r):
|
||||||
|
return f"<<<<<<< SEARCH\n{s}\n=======\n{r}\n>>>>>>> REPLACE"
|
||||||
|
|
||||||
|
|
||||||
|
D = "Он вышел за дверь. Небо было серым; вздохнув, он смотрел, как дорога уходит вдаль.\n\nОн вышел за дверь."
|
||||||
|
# Тексты с символами, чья NFKC-форма ДЛИННЕЕ одного знака — тот самый класс, что был пропущен.
|
||||||
|
NF = "Он ждал… потом ушёл. Небо было серым."
|
||||||
|
NF2 = "Цена ½ и fiрма№1… Небо было серым."
|
||||||
|
|
||||||
|
CASES = [
|
||||||
|
("простая замена", D, op("Небо было серым", "Небо было свинцовым"), dict(applied=1), "свинцов"),
|
||||||
|
("NO_CHANGE", D, "NO_CHANGE", dict(applied=0, no_change=True), None),
|
||||||
|
("якорь не найден", D, op("Небо было алым", "x"), dict(rejected_notfound=1, applied=0), None),
|
||||||
|
("неоднозначный якорь", D, op("Он вышел за дверь.", "Он шагнул наружу."), dict(rejected_ambiguous=1, applied=0), None),
|
||||||
|
("толерантность к кавычкам/тире", 'Он сказал: «да» — тихо.', op('Он сказал: "да" - тихо.', 'Он молчал.'), dict(applied=1), "молчал"),
|
||||||
|
("толерантность к пробелам", D, op("Небо было\nсерым", "Небо было свинцовым"), dict(applied=1), "свинцов"),
|
||||||
|
("малформед", D, "Вот отредактированный текст: бла-бла", dict(malformed=True, applied=0), None),
|
||||||
|
("две непересекающиеся", D, op("Небо было серым", "Небо было свинцовым") + "\n" + op("уходит вдаль", "убегает к горизонту"), dict(applied=2), "горизонту"),
|
||||||
|
# --- класс, пропущенный первой редакцией теста (ФИКС-1) ---
|
||||||
|
("NFKC: многоточие ПЕРЕД якорем", NF, op("Небо было серым.", "Небо было свинцовым."), dict(applied=1), "Небо было свинцовым."),
|
||||||
|
("NFKC: дробь/лигатура/№ перед якорем", NF2, op("Небо было серым.", "Небо было свинцовым."), dict(applied=1), "Небо было свинцовым."),
|
||||||
|
("NFKC: якорь СОДЕРЖИТ многоточие", NF, op("Он ждал… потом ушёл.", "Он не дождался."), dict(applied=1), "Он не дождался."),
|
||||||
|
# --- удаление (ФИКС-2) ---
|
||||||
|
("удаление: пустая замена", D, "<<<<<<< SEARCH\n; вздохнув, он смотрел\n=======\n>>>>>>> REPLACE", dict(applied=1, ops_total=1), None),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def invariant_ok(draft: str, res, ops_pairs) -> bool:
|
||||||
|
"""Текст вне заменённых спанов обязан сохраниться: проверяем, что каждый НЕтронутый кусок
|
||||||
|
черновика присутствует в выходе в том же порядке."""
|
||||||
|
if res.applied == 0:
|
||||||
|
return res.text == draft
|
||||||
|
keep = draft
|
||||||
|
for s, _ in ops_pairs:
|
||||||
|
k = fold(s)
|
||||||
|
if not k:
|
||||||
|
return False
|
||||||
|
# грубая, но независимая проверка: длина выхода = длина черновика ± сумма дельт замен
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ok = True
|
||||||
|
for name, draft, reply, exp, must in CASES:
|
||||||
|
r = apply_ops(draft, reply)
|
||||||
|
bad = [f"{k}={getattr(r, k)}≠{v}" for k, v in exp.items() if getattr(r, k) != v]
|
||||||
|
if must and must not in r.text:
|
||||||
|
bad.append(f"в выходе нет {must!r}")
|
||||||
|
# ИНВАРИАНТ: ничего не продублировано и не склеено
|
||||||
|
if r.applied and len(r.text) > len(draft) + 200:
|
||||||
|
bad.append("выход подозрительно длиннее черновика")
|
||||||
|
print((" OK " if not bad else " ПРОВАЛ ") + name + ("" if not bad else " <- " + "; ".join(bad)))
|
||||||
|
ok &= not bad
|
||||||
|
|
||||||
|
# прицельная проверка ФИКС-1: замена НЕ должна дублировать соседние знаки
|
||||||
|
r = apply_ops(NF, op("Небо было серым.", "Небо было свинцовым."))
|
||||||
|
expected = "Он ждал… потом ушёл. Небо было свинцовым."
|
||||||
|
good = r.text == expected
|
||||||
|
print((" OK " if good else " ПРОВАЛ ") + f"ФИКС-1 побайтно: {r.text!r}")
|
||||||
|
ok &= good
|
||||||
|
|
||||||
|
# лосслесс вне спана
|
||||||
|
r = apply_ops(D, op("Небо было серым", "Небо было свинцовым"))
|
||||||
|
good = r.text.endswith("Он вышел за дверь.") and r.text.count("Он вышел за дверь.") == 2
|
||||||
|
print((" OK " if good else " ПРОВАЛ ") + "лосслесс вне спана")
|
||||||
|
ok &= good
|
||||||
|
|
||||||
|
# удаление реально удаляет
|
||||||
|
r = apply_ops(D, "<<<<<<< SEARCH\n; вздохнув, он смотрел\n=======\n>>>>>>> REPLACE")
|
||||||
|
good = "вздохнув" not in r.text and len(r.text) < len(D)
|
||||||
|
print((" OK " if good else " ПРОВАЛ ") + f"удаление применилось (ops={r.ops_total}, applied={r.applied})")
|
||||||
|
ok &= good
|
||||||
|
|
||||||
|
print("\nВСЕ ТЕСТЫ ПРОЙДЕНЫ" if ok else "\nЕСТЬ ПРОВАЛЫ")
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
1
eval/premise_review/.gitignore
vendored
Normal file
1
eval/premise_review/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
py_batching.json
|
||||||
64
eval/premise_review/README.md
Normal file
64
eval/premise_review/README.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Ревью ПОСЫЛКИ проб 18/C/19 (полигон, 05.08.2026)
|
||||||
|
|
||||||
|
Владелец 05.08: «важно ревьювить не только результат но и посылку». Здесь — харнесс, который
|
||||||
|
проверяет **вход** экспериментов, а не их выводы: риг, промпты, метрики, константы. Всё $0,
|
||||||
|
поверх персистированного сырья; ни одного платного вызова отсюда не делается.
|
||||||
|
|
||||||
|
Повод: два подряд дефекта посылки, невидимых в результатах — аппликатор диффов молча портил
|
||||||
|
6 из 36 выходов (рапортуя `applied=ops_total, rejected=0`), и арм zh→en поехал бы поверх РУССКИХ
|
||||||
|
черновиков, потому что боевой `zh-ru/translator.md` параметризован только в первой строке.
|
||||||
|
|
||||||
|
## Что чем доказано
|
||||||
|
|
||||||
|
| Посылка | Инструмент | Итог |
|
||||||
|
|---|---|---|
|
||||||
|
| Риг пробы C воспроизводит движковые `RenderBatch`/`Batch` | `premise_batch_test.go.txt` (overlay-тест по НАСТОЯЩЕМУ движку) | 63/63 блока байт-в-байт; `DetectSeries` даёт те же 4 серии, что риг задал руками; состав батчей совпал [16,16,17,13,1], все под 6000 рун |
|
||||||
|
| Числа §C посчитаны верно | `recount.py` (свой парсер, своя нормализация, свой знаменатель — ничего не импортируется из `arbitrate.py`) | все 6 клеток воспроизвелись: glm-5 25/45 · mistral 23 · flash 20 · pro 16 · terra 18 · luna 18 |
|
||||||
|
| Посадки дефектов пробы A корректны | `strict_defects.py` | 12/12 регексов валидны (`fix_rx` ловит оригинал, `bad_rx` — нет); канон черновиков 20/21 воспроизведён |
|
||||||
|
| Метрика посадок считала ИСЧЕЗНОВЕНИЕ, а не восстановление | `strict_defects.py` | дефект метрики реален (удаление фразы = «исправлено»), но вывод НЕ двигается: строго восстановлено 47/48 клеток, «выпало» 3 |
|
||||||
|
| Запросы четырёх фальсификаций — то, что заявлено | `dryrun_arms.py` | инварианты И1–И4/E5 держатся: блок закона и черновик одинаковы в diff/full, различается ТОЛЬКО контракт; плейсхолдеров нет; арм E английский |
|
||||||
|
| Аппликатор диффов | `../editor_contract/selftest_apply.py` | 15/15, включая NFKC-классы, на которых прежняя версия ломалась |
|
||||||
|
| Константы длинной единицы | сверка с `internal/chunk/chunker.go` | `EditCeilingOut=3200` · `FertCJK=1.1978` · `FertOther=0.3852` — совпали, формула та же |
|
||||||
|
|
||||||
|
## Найденные дефекты посылки
|
||||||
|
|
||||||
|
1. **Метрика посадок пробы A** зачитывала дефект исправленным по исчезновению `bad_rx`; удаление
|
||||||
|
фразы засчиталось бы как правка. Пере-считано строго — вывод устоял.
|
||||||
|
2. **Асимметрия режимов в §C**: чемпион `glm-5` ехал `thinking:{disabled}`, а претенденты — с
|
||||||
|
разумом (`v4-pro` default, `terra`/`luna` low). В отчёте 18 §D это объявлено, но НЕ закрыто
|
||||||
|
замером; закрывается контрольным прогоном glm-5 с мышлением.
|
||||||
|
3. **`since_ch: 0`** в риге против непустого значения в проде: блок короче на 1–2 руны на кандидата,
|
||||||
|
а батч 2 стоит на 5967/6000 — в проде граница батча могла бы сдвинуться. На сравнение моделей
|
||||||
|
не влияет (батчи у всех кандидатов одни и те же).
|
||||||
|
4. **`long_units` набирает абзацы ЗА потолок** (3201 против 3200), движок же группирует ДО
|
||||||
|
превышения (`chunker.go:370`). Разница в один токен, но направление противоположное.
|
||||||
|
5. **Английский черновик** нельзя делать боевым `zh-ru/translator.md` — язык зашит прозой в
|
||||||
|
строках 9/10/14. Заведено зеркало `../editor_contract/prompts/translator-en.md`.
|
||||||
|
|
||||||
|
## Ложные тревоги (проверено, дефекта нет)
|
||||||
|
|
||||||
|
- `parse_common.py` «отсутствует» — живёт в `eval/bank_autonomy/`, путь добавляется в `sys.path`.
|
||||||
|
- `parse_common` «теряет» `aliases`/`related`/`since_ch` — этих полей в bank-stop нет вообще (0 вхождений).
|
||||||
|
- Знаменатель §A «21 вместо 27» — `元海` исключён из основной таблицы намеренно, он скорится
|
||||||
|
отдельно (в A2 инъецирован неверной формой). 27 − 6 окон = 21.
|
||||||
|
- Первая версия `dryrun_arms.py` ловила у fidelity-промпта «стиль-мандат» подстрочным регексом —
|
||||||
|
на деле это ЗАПРЕТ («Стиль… НЕ ТРОГАЙ ВООБЩЕ»). Проверка исправлена на полярность.
|
||||||
|
|
||||||
|
## Как воспроизвести
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ubuntu/projects/textmachine
|
||||||
|
# дамп ростера (НЕ коммитится: содержит KWIC-окна книги)
|
||||||
|
eval/.venv/bin/python -c "import sys;sys.path.insert(0,'eval/bank_arbitration');\
|
||||||
|
from consilium_probe import build_roster,batches_of,block_of,SERIES,BATCH_RUNES;import json;\
|
||||||
|
r=build_roster();json.dump({'roster':r,'blocks':{c['src']:block_of(c) for c in r},\
|
||||||
|
'batches':[[c['src'] for c in b] for b in batches_of(r)],'series':SERIES,'batch_runes':BATCH_RUNES},\
|
||||||
|
open('eval/premise_review/py_batching.json','w'),ensure_ascii=False,indent=1)"
|
||||||
|
# доказательство против движка (backend/ НЕ трогается — overlay)
|
||||||
|
cp eval/premise_review/premise_batch_test.go.txt /tmp/p_test.go
|
||||||
|
echo '{"Replace":{"'$PWD'/backend/internal/terminology/p_test.go":"/tmp/p_test.go"}}' > /tmp/ov.json
|
||||||
|
cd backend && go test -overlay=/tmp/ov.json -run TestPremisePythonRigMatchesEngine -v ./internal/terminology/
|
||||||
|
cd .. && eval/.venv/bin/python eval/premise_review/recount.py -v
|
||||||
|
eval/.venv/bin/python eval/premise_review/strict_defects.py edp-A0 edp-A1 edp-A2 edp-A5
|
||||||
|
eval/.venv/bin/python eval/premise_review/dryrun_arms.py
|
||||||
|
```
|
||||||
119
eval/premise_review/dryrun_arms.py
Normal file
119
eval/premise_review/dryrun_arms.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Сухая проверка ПОСЫЛКИ четырёх армов: что реально уедет на провод. Ни одного платного вызова.
|
||||||
|
|
||||||
|
Инварианты, которые обязаны держаться (иначе арм измерит не то, что заявлено):
|
||||||
|
И1 диффовый и полный контракты одного арма получают ОДИН И ТОТ ЖЕ блок закона и ОДИН И ТОТ ЖЕ
|
||||||
|
черновик — различаться должен ТОЛЬКО системный контракт, иначе сравнение не контролировано;
|
||||||
|
И2 ни в одном сообщении не осталось незаполненных `{{плейсхолдеров}}`;
|
||||||
|
И3 у арма E инструкции и черновик АНГЛИЙСКИЕ (латиница > кириллицы) — иначе повтор дефекта 05.08;
|
||||||
|
И4 у арма L reasoning_effort='low' стоит ровно на обоих контрактах, и больше нигде;
|
||||||
|
E5 у арма N системный контракт НЕ содержит стиль-мандата, а у арма D (проба 19) — содержит.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "editor_contract"))
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||||||
|
|
||||||
|
import final_probes as F # noqa: E402
|
||||||
|
|
||||||
|
FAIL = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(name: str, ok: bool, note: str = "") -> None:
|
||||||
|
global FAIL
|
||||||
|
if not ok:
|
||||||
|
FAIL += 1
|
||||||
|
print(f" [{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f" — {note}" if note else ""))
|
||||||
|
|
||||||
|
|
||||||
|
def cyr_lat(s: str) -> tuple[int, int]:
|
||||||
|
return (sum(1 for c in s if "а" <= c.lower() <= "я"),
|
||||||
|
sum(1 for c in s if "a" <= c.lower() <= "z"))
|
||||||
|
|
||||||
|
|
||||||
|
def plan_of(arm: str):
|
||||||
|
if arm == "L":
|
||||||
|
return F.plan_L()
|
||||||
|
if arm == "N":
|
||||||
|
return F.plan_N()
|
||||||
|
if arm == "G":
|
||||||
|
return F.plan_G({f"G{u}": "ЧЕРНОВИК-G" + str(u) for u in range(2)})
|
||||||
|
return F.plan_E({f"E{k}": "DRAFT-E" + str(k) for k in range(6)})
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
for arm in ("L", "N", "G", "E"):
|
||||||
|
plan = plan_of(arm)
|
||||||
|
print(f"\n=== арм {arm}: {len(plan)} вызовов ===")
|
||||||
|
tags = [p[0] for p in plan]
|
||||||
|
check("теги уникальны", len(set(tags)) == len(tags))
|
||||||
|
|
||||||
|
# И2 — плейсхолдеры
|
||||||
|
holes = set()
|
||||||
|
for tag, msgs, kind, draft, extra in plan:
|
||||||
|
for m in msgs:
|
||||||
|
holes |= set(re.findall(r"\{\{(\w+)\}\}", m["content"]))
|
||||||
|
check("И2 нет незаполненных плейсхолдеров", not holes, ", ".join(sorted(holes)))
|
||||||
|
|
||||||
|
# И1 — парность diff/full на одном материале
|
||||||
|
bykey = {}
|
||||||
|
for tag, msgs, kind, draft, extra in plan:
|
||||||
|
key = re.sub(r"^fp-.?.?", "", tag)
|
||||||
|
bykey.setdefault(key, {})[kind] = (msgs, draft)
|
||||||
|
pairs = [v for v in bykey.values() if len(v) == 2]
|
||||||
|
if pairs:
|
||||||
|
same_block = all(
|
||||||
|
[m["content"] for m in v["diff"][0] if m["role"] == "system"][1:]
|
||||||
|
== [m["content"] for m in v["full"][0] if m["role"] == "system"][1:]
|
||||||
|
for v in pairs)
|
||||||
|
same_draft = all(v["diff"][1] == v["full"][1] for v in pairs)
|
||||||
|
differ_sys = all(v["diff"][0][0]["content"] != v["full"][0][0]["content"] for v in pairs)
|
||||||
|
check(f"И1 блок закона одинаков в diff/full ({len(pairs)} пар)", same_block)
|
||||||
|
check("И1 черновик одинаков в diff/full", same_draft)
|
||||||
|
check("И1 системный контракт РАЗЛИЧАЕТСЯ", differ_sys)
|
||||||
|
else:
|
||||||
|
check("И1 арм одноконтрактный (пар нет)", arm == "N", "N — только дифф, это по дизайну")
|
||||||
|
|
||||||
|
# И4 — режим разума
|
||||||
|
efforts = {str(p[4]) for p in plan}
|
||||||
|
if arm == "L":
|
||||||
|
check("И4 reasoning_effort=low на всех вызовах",
|
||||||
|
efforts == {"{'reasoning_effort': 'low'}"}, str(efforts))
|
||||||
|
else:
|
||||||
|
check("И4 режим разума не задаётся (дефолт)", efforts == {"None"}, str(efforts))
|
||||||
|
|
||||||
|
# И3 — язык арма E
|
||||||
|
if arm == "E":
|
||||||
|
sysmsg = plan[0][1][0]["content"]
|
||||||
|
c, l = cyr_lat(sysmsg)
|
||||||
|
check("И3 инструкции английские", l > c, f"лат={l} кир={c}")
|
||||||
|
blk = plan[0][1][1]["content"]
|
||||||
|
c2, l2 = cyr_lat(blk)
|
||||||
|
check("И3 блок закона английский", l2 > c2, f"лат={l2} кир={c2}")
|
||||||
|
|
||||||
|
# E5 — стиль-мандат. ⚠ Подстрочный регекс `стил|благозвуч` здесь НЕ работает: fidelity-промпт
|
||||||
|
# называет стиль в ЗАПРЕТЕ («Стиль… НЕ ТРОГАЙ ВООБЩЕ»), и наивная проверка считала запрет
|
||||||
|
# мандатом. Отличаем по полярности: у N обязан быть явный запрет и сужение мандата.
|
||||||
|
sysmsg = plan[0][1][0]["content"]
|
||||||
|
if arm == "N":
|
||||||
|
forbids = bool(re.search(r"(стил|благозвуч|лексик)[^.]{0,80}не трогай", sysmsg, re.I))
|
||||||
|
narrow = bool(re.search(r"мандат\s+узкий|только\s+ВЕРНОСТЬ", sysmsg, re.I))
|
||||||
|
full_sys = [p[1][0]["content"] for p in F.plan_L() if p[2] == "full"][0]
|
||||||
|
positive_elsewhere = bool(re.search(r"Правь стиль", full_sys))
|
||||||
|
check("E5 у N стиль назван ЗАПРЕТОМ", forbids)
|
||||||
|
check("E5 у N мандат сужен до верности", narrow)
|
||||||
|
check("E5 у полного контракта стиль-мандат ЕСТЬ (контраст)", positive_elsewhere)
|
||||||
|
print(f" системный контракт, первая строка: {sysmsg.splitlines()[0][:100]!r}")
|
||||||
|
print(f" сообщений в вызове: {len(plan[0][1])} (роли: "
|
||||||
|
f"{[m['role'] for m in plan[0][1]]})")
|
||||||
|
|
||||||
|
print(f"\n{'ПОСЫЛКА ЧИСТА' if not FAIL else f'ПРОВАЛОВ: {FAIL}'}")
|
||||||
|
sys.exit(1 if FAIL else 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
132
eval/premise_review/premise_batch_test.go.txt
Normal file
132
eval/premise_review/premise_batch_test.go.txt
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
package terminology
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"textmachine/backend/internal/lang"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pyDump struct {
|
||||||
|
Roster []struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Freq int `json:"freq"`
|
||||||
|
Drafts [][]interface{} `json:"drafts"`
|
||||||
|
Ctx []string `json:"ctx"`
|
||||||
|
Evidence []string `json:"evidence"`
|
||||||
|
} `json:"roster"`
|
||||||
|
Blocks map[string]string `json:"blocks"`
|
||||||
|
Batches [][]string `json:"batches"`
|
||||||
|
Series map[string][]string `json:"series"`
|
||||||
|
BatchRunes int `json:"batch_runes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPremisePythonRigMatchesEngine доказывает ИСПОЛНЕНИЕМ, что питон-риг пробы C
|
||||||
|
// (consilium_probe.block_of / batches_of) воспроизводит движковые RenderBatch/Batch.
|
||||||
|
func TestPremisePythonRigMatchesEngine(t *testing.T) {
|
||||||
|
raw, err := os.ReadFile("/home/ubuntu/projects/textmachine/eval/premise_review/py_batching.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var d pyDump
|
||||||
|
if err := json.Unmarshal(raw, &d); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cands := make([]Candidate, 0, len(d.Roster))
|
||||||
|
for _, r := range d.Roster {
|
||||||
|
c := Candidate{
|
||||||
|
Key: r.Src, Src: r.Src, Type: r.Type, Origin: Origin(r.Origin),
|
||||||
|
Freq: r.Freq, SinceCh: 0, Evidence: r.Evidence, KWIC: r.Ctx,
|
||||||
|
}
|
||||||
|
for _, v := range r.Drafts {
|
||||||
|
if len(v) != 2 {
|
||||||
|
t.Fatalf("draft shape %v", v)
|
||||||
|
}
|
||||||
|
c.Variants = append(c.Variants, Variant{
|
||||||
|
Dst: v[0].(string), Chunks: int(v[1].(float64)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cands = append(cands, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (1) поблочный рендер
|
||||||
|
blockMismatch := 0
|
||||||
|
for _, c := range cands {
|
||||||
|
got := RenderBatch([]Candidate{c})
|
||||||
|
want, ok := d.Blocks[c.Src]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("нет питон-блока для %s", c.Src)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
blockMismatch++
|
||||||
|
if blockMismatch <= 3 {
|
||||||
|
t.Errorf("БЛОК РАСХОДИТСЯ %s:\n--- go ---\n%s\n--- py ---\n%s", c.Src, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("блоков сверено: %d, расхождений: %d", len(cands), blockMismatch)
|
||||||
|
|
||||||
|
// (2) движок ключ-сортирует кандидатов ДО Batch (terminology.go:212 Merge) — риг делает то же.
|
||||||
|
sort.Slice(cands, func(i, j int) bool { return cands[i].Key < cands[j].Key })
|
||||||
|
|
||||||
|
// (2а) серии движок выводит DetectSeries из язык-слоя, а риг задаёт руками. Сверяем группировки.
|
||||||
|
sEnabled, headFinal := lang.SeriesMorphology("zh")
|
||||||
|
auto := DetectSeries(cands, SeriesParams{Enabled: sEnabled, HeadFinal: headFinal})
|
||||||
|
hand := map[string]int{}
|
||||||
|
i := 0
|
||||||
|
for _, name := range []string{"zhuan", "deng", "dengzizhi", "jie"} {
|
||||||
|
i++
|
||||||
|
for _, m := range d.Series[name] {
|
||||||
|
hand[m] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups := func(m map[string]int) map[int][]string {
|
||||||
|
g := map[int][]string{}
|
||||||
|
for _, c := range cands {
|
||||||
|
if id := m[c.Key]; id != 0 {
|
||||||
|
g[id] = append(g[id], c.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
ga, gh := groups(auto), groups(hand)
|
||||||
|
t.Logf("DetectSeries(zh, enabled=%v headFinal=%v): %d серий", sEnabled, headFinal, len(ga))
|
||||||
|
for id, mem := range ga {
|
||||||
|
t.Logf(" движок серия %d: %v", id, mem)
|
||||||
|
}
|
||||||
|
for id, mem := range gh {
|
||||||
|
t.Logf(" риг серия %d: %v", id, mem)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := Batch(cands, d.BatchRunes, auto)
|
||||||
|
gotComp := make([][]string, 0, len(got))
|
||||||
|
for _, b := range got {
|
||||||
|
row := make([]string, 0, len(b))
|
||||||
|
for _, c := range b {
|
||||||
|
row = append(row, c.Src)
|
||||||
|
}
|
||||||
|
gotComp = append(gotComp, row)
|
||||||
|
}
|
||||||
|
gj, _ := json.Marshal(gotComp)
|
||||||
|
pj, _ := json.Marshal(d.Batches)
|
||||||
|
if string(gj) != string(pj) {
|
||||||
|
t.Errorf("СОСТАВ БАТЧЕЙ РАСХОДИТСЯ:\n go=%s\n py=%s", gj, pj)
|
||||||
|
} else {
|
||||||
|
sizes := ""
|
||||||
|
for _, b := range gotComp {
|
||||||
|
sizes += fmt.Sprintf(" %d", len(b))
|
||||||
|
}
|
||||||
|
t.Logf("состав батчей совпал: %d батчей, размеры%s", len(gotComp), sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) рунный размер каждого батча против потолка
|
||||||
|
for k, b := range got {
|
||||||
|
t.Logf("батч %d: %d кандидатов, %d рун (потолок %d)", k, len(b), BatchRunes(b), d.BatchRunes)
|
||||||
|
}
|
||||||
|
}
|
||||||
91
eval/premise_review/recount.py
Normal file
91
eval/premise_review/recount.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""НЕЗАВИСИМЫЙ пере-счёт пробы C: свой парсер, своя нормализация, свой знаменатель.
|
||||||
|
|
||||||
|
Цель — не «повторить 25/45», а проверить число ДРУГИМ кодом. Ничего не импортирую из
|
||||||
|
`arbitrate.py`: если унаследованный парсер систематически ошибается, повтор его же логикой
|
||||||
|
это не покажет. Совпадение чисел = подтверждение; расхождение = дефект посылки пробы C.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unicodedata
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
|
||||||
|
GOLDF = Path("/home/ubuntu/projects/textmachine/eval/bank_arbitration/gold/gold.jsonl")
|
||||||
|
|
||||||
|
|
||||||
|
def norm(s: str) -> str:
|
||||||
|
"""Нормализация цели: NFKC, регистр, ё→е, снять кавычки/дефисные варианты, схлопнуть пробелы."""
|
||||||
|
s = unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е")
|
||||||
|
s = re.sub(r"[«»\"'`]", "", s)
|
||||||
|
return " ".join(s.split())
|
||||||
|
|
||||||
|
|
||||||
|
def parse(tag: str) -> tuple[dict[str, str], float, dict]:
|
||||||
|
"""Свой парсер: строка = src <TAB> dst [<TAB> conf]. Табов нет — берём двойной пробел/'|'."""
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
cost = 0.0
|
||||||
|
diag = Counter()
|
||||||
|
for f in sorted(RAW.glob(f"{tag}-b*.json")):
|
||||||
|
rec = json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
cost += rec["cost_usd"]
|
||||||
|
for ln in rec["content"].split("\n"):
|
||||||
|
ln = ln.strip()
|
||||||
|
if not ln or ln.startswith("#") or ln.startswith("`"):
|
||||||
|
diag["skip_empty_or_comment"] += 1
|
||||||
|
continue
|
||||||
|
parts = [p.strip() for p in re.split(r"\t| {2,}|\s*\|\s*", ln) if p.strip()]
|
||||||
|
if len(parts) < 2:
|
||||||
|
diag["skip_lt2fields"] += 1
|
||||||
|
continue
|
||||||
|
src, dst = parts[0], parts[1]
|
||||||
|
# третье поле — уверенность (число или слово); в dst её быть не должно
|
||||||
|
if re.fullmatch(r"\d{1,3}", dst) and len(parts) >= 3:
|
||||||
|
diag["conf_in_second_field"] += 1
|
||||||
|
dst = parts[2]
|
||||||
|
if src in out and out[src] != dst:
|
||||||
|
diag["dup_conflict"] += 1
|
||||||
|
out[src] = dst
|
||||||
|
diag["parsed"] += 1
|
||||||
|
return out, cost, dict(diag)
|
||||||
|
|
||||||
|
|
||||||
|
def main(tags: list[str]) -> None:
|
||||||
|
gold = [json.loads(l) for l in GOLDF.read_text(encoding="utf-8").splitlines() if l.strip()]
|
||||||
|
scored = [g for g in gold if g.get("gold_dst")]
|
||||||
|
print(f"голд: строк {len(gold)}, с подписанным gold_dst {len(scored)}, "
|
||||||
|
f"из них in_bank {sum(1 for g in scored if g.get('in_bank'))}")
|
||||||
|
print()
|
||||||
|
hdr = f"{'пасс':10s} {'строк':>6s} {'точно':>6s} {'+флексии':>9s} {'нет ответа':>11s} {'$':>9s}"
|
||||||
|
print(hdr)
|
||||||
|
print("-" * len(hdr))
|
||||||
|
for tag in tags:
|
||||||
|
got, cost, diag = parse(tag)
|
||||||
|
exact = infl = missing = 0
|
||||||
|
misses = []
|
||||||
|
for g in scored:
|
||||||
|
src = g["src"]
|
||||||
|
if src not in got:
|
||||||
|
missing += 1
|
||||||
|
continue
|
||||||
|
d = norm(got[src])
|
||||||
|
if d == norm(g["gold_dst"]):
|
||||||
|
exact += 1
|
||||||
|
elif any(d == norm(a) for a in g.get("gold_alt", [])):
|
||||||
|
infl += 1
|
||||||
|
else:
|
||||||
|
misses.append((src, got[src], g["gold_dst"]))
|
||||||
|
print(f"{tag:10s} {len(got):6d} {exact:6d} {exact + infl:9d} {missing:11d} ${cost:8.6f}")
|
||||||
|
if "-v" in sys.argv:
|
||||||
|
print(f" диагностика парсера: {diag}")
|
||||||
|
for m in misses[:8]:
|
||||||
|
print(f" мимо: {m[0]} → «{m[1]}» (голд «{m[2]}»)")
|
||||||
|
print(f"\nзнаменатель для счёта: {len(scored)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main([a for a in sys.argv[1:] if not a.startswith("-")] or
|
||||||
|
["glm1", "gro1", "mis1", "ds1", "fdp", "flu", "fte"])
|
||||||
88
eval/premise_review/strict_defects.py
Normal file
88
eval/premise_review/strict_defects.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Ревью посылки пробы A: метрика посадок считала ИСЧЕЗНОВЕНИЕ, а не ВОССТАНОВЛЕНИЕ.
|
||||||
|
|
||||||
|
`editor_wire_probe.cmd_score` зачитывает дефект исправленным по `not re.search(bad_rx, out)`.
|
||||||
|
Удаление куска текста целиком тоже даёт `not still` ⇒ выпадение фразы засчитывалось как правка.
|
||||||
|
Здесь считаю три величины из того же сырья, $0:
|
||||||
|
|
||||||
|
снят = bad_rx больше не совпадает (прежняя метрика)
|
||||||
|
восстановлен = снят И fix_rx совпадает (строгая метрика)
|
||||||
|
выпал = снят, но fix_rx НЕ совпадает (кандидат в «фразу удалили»)
|
||||||
|
|
||||||
|
Плюс валидация самих регексов на исходном черновике: fix_rx ОБЯЗАН совпадать с оригиналом,
|
||||||
|
иначе строгая метрика штрафует за корректную работу.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||||||
|
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
|
||||||
|
|
||||||
|
import editor_wire_probe as P # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def out_of(tag: str):
|
||||||
|
f = RAW / f"{tag}.json"
|
||||||
|
if not f.exists():
|
||||||
|
return None
|
||||||
|
return json.loads(f.read_text(encoding="utf-8"))["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_regexes() -> int:
|
||||||
|
"""fix_rx должен ловить ОРИГИНАЛ, bad_rx — НЕ ловить его; на посаженном — наоборот."""
|
||||||
|
bad = 0
|
||||||
|
print("=== валидация регексов посадок на исходном черновике ===")
|
||||||
|
for k in sorted(P.DEFECTS):
|
||||||
|
draft = P.draft_of(k)
|
||||||
|
planted, _ = P.planted_draft(k, draft)
|
||||||
|
for cls, old, new, bad_rx, fix_rx in P.DEFECTS[k]:
|
||||||
|
fix_on_orig = bool(re.search(fix_rx, draft))
|
||||||
|
bad_on_orig = bool(re.search(bad_rx, draft))
|
||||||
|
bad_on_plant = bool(re.search(bad_rx, planted))
|
||||||
|
ok = fix_on_orig and not bad_on_orig and bad_on_plant
|
||||||
|
if not ok:
|
||||||
|
bad += 1
|
||||||
|
print(f" ⚠ w{k}/{cls}: fix_rx на оригинале={fix_on_orig} "
|
||||||
|
f"bad_rx на оригинале={bad_on_orig} bad_rx на посаженном={bad_on_plant}")
|
||||||
|
print(f" дефектных регексов: {bad} из {sum(len(v) for v in P.DEFECTS.values())}")
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
def score(arm: str) -> None:
|
||||||
|
print(f"\n=== арм {arm} ===")
|
||||||
|
for cls in ("k1", "k2"):
|
||||||
|
snyat = vosst = vypal = tot = 0
|
||||||
|
rows = []
|
||||||
|
for k in sorted(P.DEFECTS):
|
||||||
|
o = out_of(f"{arm}-w{k}")
|
||||||
|
if o is None:
|
||||||
|
continue
|
||||||
|
for c, old, new, bad_rx, fix_rx in P.DEFECTS[k]:
|
||||||
|
if c != cls:
|
||||||
|
continue
|
||||||
|
tot += 1
|
||||||
|
still = bool(re.search(bad_rx, o))
|
||||||
|
restored = bool(re.search(fix_rx, o))
|
||||||
|
if not still:
|
||||||
|
snyat += 1
|
||||||
|
if restored:
|
||||||
|
vosst += 1
|
||||||
|
rows.append(f"w{k}:восст")
|
||||||
|
else:
|
||||||
|
vypal += 1
|
||||||
|
rows.append(f"w{k}:ВЫПАЛ")
|
||||||
|
else:
|
||||||
|
rows.append(f"w{k}:жив")
|
||||||
|
if tot:
|
||||||
|
print(f" {cls}: снят {snyat}/{tot} · ВОССТАНОВЛЕН {vosst}/{tot} · выпал {vypal} "
|
||||||
|
+ " ".join(rows))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
validate_regexes()
|
||||||
|
for arm in sys.argv[1:] or ["edp-A0", "edp-A1", "edp-A2", "edp-A3", "edp-A4", "edp-A5"]:
|
||||||
|
score(arm)
|
||||||
Loading…
Add table
Reference in a new issue