Add the English pair: its prompt pack, pair layer and prompt-variant resolution, and drop the comments the Chinese prompts shipped with

This commit is contained in:
heaven 2026-09-18 03:50:48 +03:00
parent f51f4703b2
commit ddadb3e619
32 changed files with 1356 additions and 127 deletions

View file

@ -12,7 +12,7 @@
| `internal/llm` | OpenAI-совместимый транспорт + retry/backoff, **capability-слой** (budget_field/temperature/reasoning per-модель), провайдеры openai/local (no-proxy)/anthropic (DEPRECATED-референс); маршрутизация лейблами живёт в config/pipeline (канал B, один хоп `chain[0]`, fail-closed; D4/D39.26) | `httpllm.go`, `capability.go`, `provider_*.go` |
| `internal/ledger` | Цены по usage (вкл. reasoning/cache-поля), `PriceForResponse` по фактической модели | `pricing.go` |
| `internal/store` | SQLite (modernc, CGO-free), цепочка миграций `schema_version`, reserve/settle+checkpoint, chunk_status, глоссарий (+подписной цикл терминолога), ruby, retrieval_state, **`wave_selection`** (что показали каждой волне по отдельности — у `retrieval_state` ключ один на чанк, и следы двух волн складывались в одну строку), request_log (+`reasoning_in_completion`НЕ NULL-по-умолчанию колонка: сколько уже оплаченного вывода было размышлением; `NULL` = провайдер не сообщил, и это НЕ ноль); `OpenReadOnly` — без flock/миграций/recovery для status/report/export (схема не совпала — типизированный `*store.SchemaMismatchError{Found,Expected}`, обе стороны); `Migrate` (+шов `beforeApply` под локом), `SchemaHead` — та самая `Expected`; write-open БД новее бинаря — ОТКАЗ (D39.134) | `ledger.go`, `migrate.go`, `glossary.go`, `store.go` |
| `internal/config` | fail-fast загрузка models/pipeline/book; **эхо-мина-гейт** `echoMineViolation`; `CheckRunnable` блокирует неисполнимое (C2/fanout/judge); **пар-слой** (D39.23): `configs/pairs/<пара>.yaml` — калибровка нарезки + коридор длины пары; **промпт-резолв КОНВЕНЦИЕЙ** `prompts/<пара>/<роль>.md` из `Book.LangPair()`+роли, **fail-loud на пару без пакета** (ja-книга не едет молча через zh-промпт), осознанное исключение — `prompt_override` на стадии | `models.go`, `pipeline.go`, `pair.go` |
| `internal/config` | fail-fast загрузка models/pipeline/book; **эхо-мина-гейт** `echoMineViolation`; `CheckRunnable` блокирует неисполнимое (C2/fanout/judge); **пар-слой** (D39.23): `configs/pairs/<пара>.yaml` — калибровка нарезки + коридор длины пары; **промпт-резолв КОНВЕНЦИЕЙ** `prompts/<пара>/<роль>.md` из `Book.LangPair()`+роли, **fail-loud на пару без пакета** (ja-книга не едет молча через zh-промпт), вариант роли — `prompt_variant` (та же конвенция, `<роль>-<вариант>.md` внутри пары книги), осознанный путь вне конвенции — `prompt_override` | `models.go`, `pipeline.go`, `pair.go` |
| `internal/text` | Нормализация и рун-примитивы, общие для банка/майнера/чекеров (script-agnostic слой, `lang` НЕ импортирует): `NormalizeSourceKey`/`NormalizeTargetForm` (версионированный артефакт `NormVersion()`, фолдится в снапшот), `NormalizeSource`, `TokenizeScript` (алфавит цели из данных, Mn-фолд U+0301; `TokenizeCyrillic` = ru-дефолт-обёртка), `DenseScript` — единый дом sizing-таксономии (D39.64) | `norm.go`, `runes.go`, `source.go` |
| `internal/seed` | Схема сид-YAML книги (`File`/`Term`/`Decl`/`Alias`) — общий артефакт: майнер её ПИШЕТ (mined-дельта), банк ЧИТАЕТ (`LoadGlossarySeed`); тип общий, поэтому эмиссия не может разъехаться с загрузчиком | `seed.go` |
| `internal/lang` | Языковые ДАННЫЕ и пак-механизм: эмбед-плоскость `data/*.txt` (хешируется в `EmbeddedVersion()` → снапшот, D39.64) · langpack `Load`/книго-overlay (аллоулист файлов, fail-loud) · **манифест-по-каналам** (`manifest.txt` источника объявляет `source-morphology`/`transliteration`; манифест-less пак = все каналы, байт-идентичен) · единый реестр письменностей `script.go` (`LangScripts`/`IsCJKScriptLang` из `lang-script.txt`) · реестр целей из `data/target-<tgt>.txt` | `langpack.go`, `embedded.go`, `script.go` |

View file

@ -6989,5 +6989,166 @@
"replace": "\t\t\told[row.FPVersion]++\n\t\t\tchanged++\n\t\t\tcontinue\n\t\t}"
}
]
},
{
"id": "EN-variant-resolves-in-the-books-pair",
"battery": true,
"package": "./internal/config/",
"why": "prompt_variant exists to bind a role variant to the BOOK's pair; resolving it against a fixed pair is the latent 'a book rides another pair's conventions' bug wearing the new key",
"edits": [
{
"file": "internal/config/pipeline.go",
"find": "\t\t\tcp := promptConventionPath(promptsRoot, pair, st.Role, st.PromptVariant)",
"replace": "\t\t\tcp := promptConventionPath(promptsRoot, \"zh-ru\", st.Role, st.PromptVariant)"
}
]
},
{
"id": "EN-variant-and-override-both-accepted",
"battery": true,
"package": "./internal/config/",
"why": "two keys stating different intents must not silently pick a winner; a precedence rule makes the loser dead without saying so",
"edits": [
{
"file": "internal/config/pipeline.go",
"find": "\t\tif st.PromptOverride != \"\" && st.PromptVariant != \"\" {",
"replace": "\t\tif false && st.PromptOverride != \"\" && st.PromptVariant != \"\" {"
}
]
},
{
"id": "EN-variant-accepts-a-path",
"battery": true,
"package": "./internal/config/",
"why": "a separator or `..` in prompt_variant turns it back into a path and smuggles back the pair-fixing the key exists to end",
"edits": [
{
"file": "internal/config/pipeline.go",
"find": "\t\t\tif strings.ContainsAny(v, `/\\`) || strings.Contains(v, \"..\") || strings.TrimSpace(v) != v {",
"replace": "\t\t\tif false {"
}
]
},
{
"id": "EN-variant-missing-file-is-silent",
"battery": true,
"package": "./internal/config/",
"why": "a missing variant must stop the load naming pair/role/variant; a silent pass leaves the stage pointing at a file that is not there",
"edits": [
{
"file": "internal/config/pipeline.go",
"find": "if _, serr := os.Stat(cp); serr != nil {",
"replace": "if serr := error(nil); serr != nil {"
}
]
},
{
"id": "EN-shipping-config-pins-the-pair-in-a-path",
"battery": true,
"package": "./internal/config/",
"why": "the DATA half of the same property: a shipping config naming `../prompts/zh-ru/...` hands an English book the Chinese draft prompt, and until a second pack existed nothing said so",
"edits": [
{
"file": "configs/pipeline-c1.yaml",
"find": " prompt_variant: banknote",
"replace": " prompt_override: ../prompts/zh-ru/translator-banknote.md"
}
]
},
{
"id": "EN-pair-fertility-falls-back-to-zh",
"battery": true,
"package": "./internal/config/",
"why": "without the pair's own estimate the English book is chunked on the zh-ru calibration, and a fallback is indistinguishable from a choice at every later point",
"edits": [
{
"file": "configs/pairs/en-ru.yaml",
"find": " other: 0.4203",
"replace": " other: 0"
}
]
},
{
"id": "EN-override-traverses-out-of-the-pair-dir",
"battery": true,
"package": "./internal/config/",
"why": "the pair gate used a substring test over an uncleaned path, so `../prompts/en-ru/../zh-ru/x.md` satisfied it while the English book ran the Chinese prompt; the predicate is now the parent DIRECTORY",
"edits": [
{
"file": "configs/pipeline-c1.yaml",
"find": " prompt_variant: banknote",
"replace": " prompt_override: ../prompts/en-ru/../zh-ru/translator-banknote.md"
}
]
},
{
"id": "EN-shipping-config-stops-serving-a-pair",
"battery": true,
"package": "./internal/config/",
"why": "the pair gate counted loads PER PAIR, so a config that stopped loading removed itself from the gate in silence; the count is now per (config, pair) with the shortfall named",
"edits": [
{
"file": "configs/pipeline-c2.yaml",
"find": " prompt_variant: selector",
"replace": " prompt_variant: nosuchvariant"
}
]
},
{
"id": "EN-prompt-moves-under-a-stationary-label",
"battery": true,
"package": "./internal/pipeline/",
"why": "the byte gate walked one hardcoded pair, so the second prompt pack could be rewritten under a stationary prompt_version with the whole battery green; it now walks every pair pack on disk",
"edits": [
{
"file": "prompts/en-ru/translator.md",
"find": "Буквальность здесь не достоинство.",
"replace": "Переводи как можно буквальнее."
}
]
},
{
"id": "EN-banknote-body-drifts-from-the-translator-it-copies",
"battery": true,
"package": "./internal/pipeline/",
"why": "an edit that lands in ONE of a pair's two translator files and updates the label ledger the documented way leaves the pair shipping two different translators — c1 and the arms render with the banknote file, c2 with the plain one — and the byte gate PASSES on exactly this planting, measured on a copy ⚠ both anchors are VALUE-coupled: the ledger sha must be re-taken whenever this prompt is edited, and the catalogue's anchor sweep says so loudly",
"edits": [
{
"file": "prompts/en-ru/translator.md",
"find": НЕ пересчитывай в метры",
"replace": "и пересчитывай в метры"
},
{
"file": "internal/pipeline/testdata/prompt-labels.json",
"find": "439e725c4940a653856c6bd03f3a94e1985899f4bf89e3c0c5a135533c3ef9ad",
"replace": "d599b7339012164032706c6b4f2cdf8738155f605d41d50cbda25b649c2ca8bc"
}
]
},
{
"id": "EN-variant-branch-stops-naming-the-variant",
"battery": true,
"package": "./internal/config/",
"why": "the new fail-loud branch that names the VARIANT was the one new branch of this pack with no planting, and its pin was satisfied by the printed path — 'nosuch' is a substring of translator-nosuch.md, so the message could stop naming a variant with the assertion still green",
"edits": [
{
"file": "internal/config/pipeline.go",
"find": "what = fmt.Sprintf(\"role %q variant %q\", st.Role, st.PromptVariant)",
"replace": "what = fmt.Sprintf(\"role %q\", st.Role)"
}
]
},
{
"id": "EN-production-pair-layer-drifts-unseen-by-the-english-pin",
"battery": true,
"package": "./internal/config/",
"why": "the zh half of the en-ru pair-layer pin asserted only numbers that are ALSO the engine fallback, so it could not tell a read pair layer from a vanished one. MEASURED: three pins in this package red on this planting (TestPairLayerCarriesCalibration and TestEveryShippingConfigLoadsUnderThePairLayer already guarded the corridor), so the gap was in the PIN's wording, not a hole in the tree — the entry records which pins fire",
"edits": [
{
"file": "configs/pairs/zh-ru.yaml",
"find": "len_ratio_bounds: [2.2, 4.2]",
"replace": "len_ratio_bounds: [2.2, 4.3]"
}
]
}
]

View file

@ -1,6 +1,29 @@
# Пар-слой en→ru. Как и ja-ru: своей калибровки нарезки нет, пакета промптов prompts/en-ru/ нет
# (книга на этой паре упадёт громко на загрузке).
# Пар-слой en→ru. Промпты резолвятся конвенцией prompts/<пара>/<роль>.md (каталог prompts/en-ru/ есть).
# ⛔ Каталога configs/langpacks/en-ru/ НЕТ НАМЕРЕННО: загрузчик включается по существованию каталога
# пары и затем требует у источника `en` либо одиннадцать таблиц КИТАЙСКОЙ морфологии, либо пиньинь —
# любой файл здесь делает каждую английскую книгу незапускаемой. Носитель — строка бэклога 347;
# замер и цена — backend/docs/EN_RU_PAIR_BASIS.md.
pair: en-ru
# ⛔ ОЦЕНКА, НЕ КАЛИБРОВКА: прогонов на английской книге не было. Заведена потому, что generic-фолбэк
# (pipeline.go, 0.3852) — по его же комментарию zh-ru пар-калибровка, и без блока английская книга
# считалась бы китайскими числами.
# other = r / 3.64, где r — токенов ru-выхода на токен en-исходника, 3.64 — знаков en без пробелов
# на токен (experiments/01-token-calibration.md). r=1.53 у DeepSeek (боевые стадии) → 0.4203.
# ⚠ Число МОДЕЛЬНО-ЗАВИСИМО: GLM 0.3846, Qwen 0.4615 — открытый предмет строки бэклога 14. Ошибка
# смещена в безопасную сторону: 0.4203 > 0.3852 ⇒ чанк меньше, риск `length` ниже.
segmentation:
fertility:
# `cjk` не объявлен: dense-рун у английского источника ноль (351 юнит из 351 на живой книге),
# коэффициент умножается на ноль. Фолбэк 1.1978 верен по роду для китайской цитаты внутри книги.
other: 0.4203
# draft_budget_out / edit_ceiling_out не объявлены: они в ru-токенах ВЫХОДА, то есть на целевой
# стороне, и от языка источника не зависят. Фолбэк 1797/3200.
# Коридор len_ratio (символы без пробелов) для эксцизия-гейта; сам гейт ран-скоупный и в c1 выключен.
# Границы стояли здесь до пака и запинены internal/checks/coverage_test.go. Арифметическая сверка:
# 1.53 × 2.40/3.64 ≈ 1.01 у DeepSeek, 1.40 × 2.67/3.64 ≈ 1.03 у GLM — ожидание около 1.0, не у края.
# Прямого замера на парных текстах нет: русские переводы книги в дереве есть, их исходников нет.
coverage:
len_ratio_bounds: [0.70, 1.4]

View file

@ -4,7 +4,8 @@
#
# Промпты пары НЕ перечисляются: они резолвятся КОНВЕНЦИЕЙ prompts/<пара>/<роль>.md
# (prompts/zh-ru/translator.md, prompts/zh-ru/editor.md). Осознанный вариант роли — через
# prompt_override на стадии арма (напр. prompts/zh-ru/editor-mono.md).
# prompt_variant на стадии арма (напр. `prompt_variant: mono` → prompts/<пара>/editor-mono.md);
# prompt_override — только для пути ВНЕ конвенции (фикстура, разовый арм).
pair: zh-ru
# WS2 (слой 1): бюджет нарезки в ВЫХОДНЫХ (ru) токенах через фертильность

View file

@ -35,7 +35,8 @@ stages:
- name: draft
role: translator
model: deepseek-v4-flash
prompt_override: ../prompts/zh-ru/translator-banknote.md
# Вариант роли, не путь — резолвится конвенцией внутри пары КНИГИ (довод в pipeline-c1.yaml).
prompt_variant: banknote
prompt_version: v2-reflow-nogenre-banknote
temperature: 0.3
reasoning: "low"

View file

@ -39,7 +39,8 @@ stages:
- name: draft
role: translator
model: deepseek-v4-flash
prompt_override: ../prompts/zh-ru/translator-banknote.md
# Вариант роли, не путь — резолвится конвенцией внутри пары КНИГИ (довод в pipeline-c1.yaml).
prompt_variant: banknote
prompt_version: v2-reflow-nogenre-banknote
temperature: 0.3
reasoning: "low"

View file

@ -64,17 +64,19 @@ stages:
# (Book.LangPair()) и роли стадии — здесь prompts/zh-ru/translator.md. Пути в конфиге больше нет:
# добавить пару = положить каталог. Книга на паре без пакета промптов падает ГРОМКО на загрузке,
# называя ожидаемый путь (закрывает латентный баг «ja едет через китайский паратаксис»).
# Осознанный вариант роли (напр. editor-mono) — через prompt_override на стадии.
# Осознанный ВАРИАНТ роли (напр. editor-mono) — через prompt_variant на стадии: он резолвится
# той же конвенцией, `<prompts root>/<пара>/<роль>-<вариант>.md`, то есть внутри каталога пары
# КНИГИ. Ключ prompt_override (явный путь, пар-агностичный) остаётся для фикстур и разовых армов.
# D30.2 reflow: из translator.md убрано «Сохраняй разбивку на абзацы» (построчная
# вёрстка исходника деструктивна для ru — корень нечитаемости exp12 §2.5-S). Bump
# v0-draft→v1-reflow: сменил SHA промпта, осознанный --resnapshot.
# Банкнотный вариант той же роли: translator.md + двенадцать строк инструкции про ⟦TM-BANK-v1⟧.
# Осознанное исключение из конвенции пути (D39.23), ровно та форма, которую конвенция для него и
# держит. ⚠ Лейбл ОБЯЗАН отличаться от лейбла того же role в конфиге, который везёт ПЛОСКИЙ
# translator.md — сегодня это только c2 (армы пере-сняты копией этого файла 07.09 и везут банкнотный
# вариант под этим же лейблом): один лейбл над разными байтами — это и есть дефект, который
# TestPromptLabelsPinTheirBytes ловит.
prompt_override: ../prompts/zh-ru/translator-banknote.md
# Банкнотный вариант той же роли: translator.md + инструкция про ⟦TM-BANK-v1⟧.
# Вариант роли, не путь: `prompt_variant` резолвится конвенцией внутри пары КНИГИ. Прежний
# `prompt_override: ../prompts/zh-ru/…` зашивал пару в литерал, и на второй паре конфиг
# загружался с китайским черновым промтом — см. TestEveryShippingConfigResolvesInsideItsOwnPair.
# ⚠ Лейбл prompt_version обязан отличаться от лейбла того же role с ПЛОСКИМ translator.md:
# один лейбл над разными байтами ловит TestPromptLabelsPinTheirBytes.
prompt_variant: banknote
prompt_version: v2-reflow-nogenre-banknote
temperature: 0.3
# D39.92 / row 112: "off" here is an ABSENT key (deepseek-v4-flash is ReasoningNone), so the call rode
@ -143,7 +145,7 @@ gates:
# WS4, ратифицирован D39.10. ПРЯМОЙ канал dst: переводчик после текста дописывает ⟦TM-BANK-v1⟧ и
# таблицу новых терминов фрагмента — то, что ко-оккурентный майнер не извлекает по построению
# (蛊→гу). Включение — ДВА согласованных изменения, и второе стоит на стадии draft выше:
# `prompt_override` на банкнотный вариант промпта, потому что сам гейт лишь СРЕЗАЕТ блок, а
# `prompt_variant: banknote` на стадии draft, потому что сам гейт лишь СРЕЗАЕТ блок, а
# инструкцию его писать несёт промпт. Гейт фолдится в снапшот (`banknoteSnap`), промпт — через
# PromptSHA256; оба движения санкционированы окном пере-снапшота этого батча (D39.190 п.4).
enabled: true

View file

@ -38,9 +38,9 @@ stages:
- name: select
role: judge
model: glm-5
# Роль judge держит промпт judge-selector.md — имя файла НЕ совпадает с ролью, поэтому здесь
# осознанный prompt_override вместо конвенции prompts/<пара>/<роль>.md (пак-15).
prompt_override: ../prompts/zh-ru/judge-selector.md
# Имя файла не равно имени роли — это ВАРИАНТ роли: `selector` резолвится в
# `<prompts root>/<пара>/judge-selector.md` (довод в pipeline-c1.yaml).
prompt_variant: selector
prompt_version: v0-skeleton
temperature: 0
# ⚠ DO NOT FLIP. Row 112 and the standards prompt both address this line as "the draft stage"; it is

View file

@ -0,0 +1,319 @@
# Пара en→ru: на чём стоит каждое число и решение
Носитель провенанса пар-пакета. Промты его не несут: комментариев в промтах нет по слову владельца
17.09 («не нужны ни в каком виде»), а знание обязано жить в репозитории.
## 1. Что пара везёт
| файл | что в нём |
|---|---|
| `prompts/en-ru/translator.md` | рамка двух браков + пять осей, русская и английская стороны |
| `prompts/en-ru/translator-banknote.md` | тело переводчика + канал ⟦TM-BANK-v1⟧: та же системная часть плюс ОДНА вставка, различие стережёт пин `TestBanknotePromptIsItsTranslatorPromptPlusOneBlock` — правку веди в ОБА файла |
| `prompts/en-ru/editor.md` | шесть осей правки, десять проверок, ДИСКУРС-перевёрстка, два FEWSHOT — ⚠ блок примеров едет только на АРМАХ: `c1`/`c2` держат `few_shot: false` (D38.5, reasoning-редактор), и на боевом пути модель их не видит |
| `prompts/en-ru/terminologist.md` | имена по языку происхождения, топонимы по составу, титулы по смыслу |
| `prompts/en-ru/classifier.md` | класс × род; cue-слова английские, `they` об одном лице → `none` |
| `prompts/en-ru/judge-selector.md` | роль не исполняется фазой 0; ⚠ «выведи только номер» — ЗАГЛУШКА до парсера фазы 2 |
| `prompts/en-ru/repair/latin_residue.md` | ⟦TM-NOCHANGE⟧ — главная ветка; имя чинится только при очевидной форме |
| `prompts/en-ru/repair/broken_word.md` | целевой класс, копия zh: дефект русского выхода от источника не зависит |
| `configs/pairs/en-ru.yaml` | оценка `fertility.other` + коридор |
## 2. Почему промт вообще стоит работы — замер, а не вкус
`docs/experiments/23-editor-tier.md:5536` — разрывы в долях собственного порога, панель `p4`,
16 английских глав этой книги, 16 отдельных слепых читателей:
```
удалить ВЕСЬ ролевой промт (RE↔RQ) 1.50 / 1.13 ≈ 1.3 порога ⚠ см. поправку (б) ниже
удалить дорогую стадию (Z0↔ZD) 1.38 / 0.85 ≈ 1.6 порога
```
Первичный эндпойнт объявлен ДО чисел (`:5530`): 13 глав из 16 в одну сторону, знаковый p=0.0213.
⇒ ролевой промт по весу сопоставим с целой платной стадией.
## 3. Рамка двух браков — решения ВЛАДЕЛЬЦА, не наши
Формулировка взята из `eval/dovodka/rol.py` (второе определение `_BRAK`, после приёмки), её основания:
- 16.08, дословно: «штрафовать за вольность нельзя, живой язык — приоритет; штрафовать можно только
за перевирания смысла» (`eval/dovodka/prompts/onepass-core.md:13`). Там же: «Ни один боевой промт
такого разрешения не давал — оба только ЗАПРЕЩАЛИ».
- 20.08, дословно: «присутствие одного языка в другом… полное отсутствие художественности и родного
звучания… машинный перевод грубо говоря… перевод это отчасти какое-то но творчество, без
перевирания смысла нужно написать текст, чтоб он звучал естественно» (`eval/dovodka/rol.py:187-190`).
- Слепое чтение 17.08 забраковало БОЕВОЙ перевод по РЕГИСТРУ: ярость передана словом «шествовала»,
мат смягчён, вежливое «Пойдёмте» в реплике, которую выплёвывают (`eval/dovodka/prompts/onepass-core.md:15-19`).
- Абляция редакций: `RB > Z0 > RC > RN` (`eval/dovodka/SESSION2-LOG.md` §15.21), RN дороже всех по
размышлению (28 809 против 23 301, §15.19). ⚠ **Контроль шума КРАСНЫЙ** — порядок при n=3 не
разрешим; уцелевает только «RB не хуже продакшена».
В боевых `prompts/zh-ru/` этих решений НЕТ. Внесение туда — деньги (см. §6), решение оркестратора.
⛔ **И ЧЕГО ЭТА РАМКА МОЖЕТ НЕ ДЕЛАТЬ — назвать обязательно, иначе первый же замер на английском
припишет промту то, что сделал ТАРИФ.** Полигон записал механизм победы `RB` прямо
(`eval/dovodka/SESSION2-LOG.md:1392`, сверено мной): «`RB` думает 23 301 токен и пишет 94100% текста
в уме — то есть покупает качество той же дорогой монетой. Дешёвый `RC` (11 268) написал хуже. **Качество
здесь пропорционально не удачности формулировки, а количеству черновиков, которые модель успела
написать про себя.**» Цена клетки: `RB` $0.1050 против ~$0.025 у боевой связки — вчетверо (`:1299-1300`).
**переносится ФОРМУЛИРОВКА, а работал, возможно, БЮДЖЕТ РАЗМЫШЛЕНИЯ.** На en→ru при другом усилии та
же рамка может не дать ничего. Наверняка переносится только одно: это решение ВЛАДЕЛЬЦА.
⚠ Поправка к красному контролю шума, прочитанному мной слабее, чем он есть: читатель НЕ спутал
близнецов — он написал, что `ZF` «маскируется под `Z0`», родство опознал и всё равно развёл арма на 2-е
и 5-е место ⇒ это разброс АРМА, а не шум читателя.
⛔ **ДВЕ ПОПРАВКИ, КОТОРЫЕ ПРИШЛИ ПОЗЖЕ И ОТМЕНЯЮТ ДВА МОИХ УТВЕРЖДЕНИЯ.**
**(а) Механизм дефекта ингеста я назвала НЕВЕРНО ДВА РАЗА, и оба раза со словом «проверено».** Сперва:
«движок отдаёт перенос на каждый уровень вложенности `<blockquote>`» — избыток пустых строк БЕЗВРЕДЕН,
`splitParagraphs` (`internal/chunk/chunker.go:470-478`) их отбрасывает, а `blockTags` держит `blockquote`
НАМЕРЕННО (без него «a whole chapter collapses into one paragraph»). Затем: «`<p>` внутри `<blockquote>`
это строка стиха». ⛔ Пере-снято моим прибором прямо по EPUB (126 документов): **`<p>` внутри
`<blockquote>` — 0**. Такого механизма нет.
**Что в книге ЕСТЬ, замер тем же прибором:** цепочек `blockquote`**88**, пустых 0, **87 из 88 несут
ровно ОДНУ строку**, медиана длины **32 знака**; максимальная вложенность 10 и она означает типографский
отступ. Шесть подряд: «And so ended the dark year of» · «Empire 676, wherein the Fivefold» · «Throne of
Elidaen was beset by» … ⇒ это **прозаический эпиграф, разбитый конвертером на строки по ~32 знака**, каждая
своей цепочкой; движок отдаёт каждую отдельным абзацем. Правило промта поэтому ВЕРНО по выводу и
пере-формулировано по признаку: «несколько коротких абзацев подряд, каждый продолжает предыдущий» — без
опоры на знаки конца предложения (они у пар разные, и такой признак ветвился бы по языку).
⚠ Число «726 абзацев без знака конца предложения» о стихе НЕ говорит: разложение показало заголовки,
обрывки оглавления и реплики, оборванные тире. Ряд 499 пере-формулирует оркестратор.
**(б) `restore_paragraphs` мне НЕ подходит, и совет взять его отозван его же автором.** Он чинил дефект
ПОЛИГОННОГО скрипта (`pair_en.raw_text` сносил теги регексом), а не движка.
**НО СЛЕДСТВИЕ ТОГО ПОЛИГОННОГО ДЕФЕКТА ОСТАЁТСЯ, И ОНО БЬЁТ ПО ЧИСЛАМ ВЫШЕ.** Их собственная
диспозиция «починка экстрактора абзацев — до любого нового английского замера, весь материал искажён нами»
(`eval/dovodka/KONSILIUM-22-08.md:141`) НЕ исполнена, арм `RA` НЕ куплен. ⇒ **английские СУДЕЙСКИЕ числа
полигона сняты на тексте с потерянными границами абзацев**, и это касается ровно двух чисел, на которые я
ссылаюсь: вес ролевого промта (§2) и «редактор на английском покупает» (1.22 против 2.78, §1 таблицы).
Оба помечены в тексте. ⚠ Плюс их же оговорка о приборе: разрешение рангового прибора на английской паре
**2.15 места против 1.63 на китайской** — на en-стороне «неразличимо» наступает раньше.
**УТОЧНЕНИЕ ОТ САМОГО ПОЛИГОНА, и оно возвращает основание:** дефект подачи был обойдён ОСОЗНАННО
вход у ВСЕХ армов одинаково плоский (`eval/dovodka/rol.py:110-127`: «`RN / ZP` — эффект ПРОМТА, вход у обоих
ОДИНАКОВЫЙ, плоский; `RA / RN` — эффект ПОДАЧИ»), 273 английские клетки, у восстановленного арма `RA` — 0.
**контрасты промтов между собой ЧИСТЫ**, и порядок `RB > RC > RN` держится. Не держится АБСОЛЮТНЫЙ уровень
качества и **всё, что про абзацный строй и оформление диалога**: модель видела сплошной блок вместо 11 447
абзацев. ⚠ Мои правила об абзацах и диалоге на их английских наблюдениях НЕ стоят — они стоят на
`docs/research/12-failure-modes-ru-target.md` §10 (норма Розенталя) и на МОИХ замерах прибором движка
(2370 из 2532 абзацев, 11 203 абзаца по всем главам).
**И факт, который надо записать раньше, чем он понадобится:** если бэкенд починит ингест, английские числа
полигона и новые станут НЕСОПОСТАВИМЫ (не «неверны», а сняты на разном тексте). Причина, по которой полигон
не починил свой экстрактор, — цена: uid единицы = `sha1(source.strip())`, правка пробелов переименовывает
все единицы и **289 оплаченных клеток на $2.64 теряют адресата**.
⇒ ЧТО ЭТИМ НЕ ЗАТРОНУТО: подсчёты по тексту книги (`feet` ×161, `François` ×517 и прочие) — это счёт, а не
судейство; абляция рамки куплена на ZH (`--buy zh RC RB`), не на английской панели; постинструкция A1
мерена на zh и grok; мои собственные замеры движком (2370 из 2532 абзацев и т. д.).
## 4. Правила русской стороны — носители
Все из `docs/research/12-failure-modes-ru-target.md`: ты/вы как событие §2 · вид §3 · рема в конец §4 ·
деепричастие с общим субъектом §5 · несобственно-прямая речь без бэкшифта §6 · диалект компенсацией
§7 · брань §8 · междометия §9 · тире-вёрстка диалога §10. Блок-лист транслит-междометий уже лежит в
данных: `internal/lang/data/target-ru.txt`, ключ `translit_interjection`.
⚠ Правило «плотность жестов» у ПЕРЕВОДЧИКА снято: за ним не стоит замера, а прореживание требует
сверки с исходником — это работа редактора. У редактора оставлено.
## 5. Английская сторона — числа по этой книге
Подсчёты полигона (`docs/experiments/21-role-topology.md`) и мои по EPUB (вхождения, не строки):
`François` ×517 · `Fabién` ×180 · `château` ×93 · `Aveléne` ×21 · `Niamh` ×15 · `feet` ×161 ·
`miles` ×25 · `inch` ×16 · `fathom` ×4 · `fucking` ×136.
**`stone` ×228 — и ни одного раза МЕРОЙ ВЕСА.** Единственное вхождение с числительным — «five stone
bowls», то есть прилагательное. ⇒ правило «отличай stone-меру от stone-камня» снято из переводчика и
редактора: оно требовало различения там, где различать нечего, и тем самым ставило под сомнение 228
законных «камней». Это частный случай ратифицированного правила владельца (17.09): данные пары обязаны
так же хорошо отрабатывать ТАМ, ГДЕ ЯВЛЕНИЯ НЕТ, — правило либо про КЛАСС без опоры на конкретную меру,
либо уходит. Рамочный рассказ: в крупнейшей главе движка 2370 абзацев из 2532
открываются «“», закрывающая «”» стоит у 183 — непарная кавычка есть КОНВЕНЦИЯ, а не ошибка набора.
Примеры в промтах ролей, чей ответ становится законом банка (терминолог, классификатор, банкнота),
взяты ВНЕ книги. Замер по САМИМ ПРИМЕРАМ, а не по именам вообще: двадцать пять терминов-примеров
(`Ashlyn Vale`, `the Ashen Reach`, `the Long Dark`, `the Quiet One`, `Michel du Bois`, `ORourke`,
`Corwin`, `Highmarch`, `Ravenshire`, `kilnstone`, `wyrmling`, `drakeling`, `Marchwarden`, `Marie-Claire`,
`Sévrine`, `Thibault`, `Oisín`, `the Elder`, `Gaspard`, `Lord Marshal`, `Brother Confessor`,
`the Ninth Canticle`, `Saint-Aubin`, `Ashlyn`, `Sable`) — в тексте книги **0 вхождений каждый**; прибор
прочёл 1 626 475 рун, контроль на том же тексте: `François` **517**. Последние семь нашёл советчик — мой
список был на семь короче, и это разница между «посчитала примеры» и «посчитала ВСЕ примеры». ⚠ Отдельные СЛОВА из примеров в книге, разумеется, есть (`Vale` 45,
`Reach` 23, `Dark` 29) — законом банка становится строка-термин целиком, и спрашивать надо о ней.
## 6. Цены, которые надо знать прежде, чем править
- **Снятие комментариев из промта — $0 ТОЛЬКО ПРИ УСЛОВИИ, и условие стоит одного переноса строки.**
`stripPromptComments` (`internal/pipeline/render.go:104-120`) снимает РОВНО `<!--…-->` и оставляет
окружающие пробелы; SHA берётся по всей очищенной форме ДО разбора и до `TrimSpace`. Замер функцией
движка на `classifier.md`, три операции над одним файлом: снятие ТОЛЬКО тега — `885766fd1e37`
(хеш не двигается); тег вместе с ОДНИМ хвостовым переносом — `82759ef35326`; тег вместе с ДВУМЯ —
`f7bc660330a3`. ⚠ Первая редакция этого абзаца печатала `f7bc660330a3` под словами «вместе с хвостовым
переносом», то есть приписывала число не той операции; пере-снято.
⚠ Хеш промта чувствителен к пробелам, которых модель НЕ ВИДИТ (`System` проходит `TrimSpace`).
НО ЦЕНА ЭТОГО НЕ ВЕЗДЕ ОДНА, и первая редакция этого раздела сказала шире, чем держится.
Потребителей файлового `tpl.SHA256` ровно два: `internal/pipeline/snapshot.go:495` (промты СТАДИЙ) и
`:169` (классы РЕМОНТА). Банк-роли туда НЕ входят — они опознаются ОТРЕНДЕРЕННЫМ `tpl.System`
(`internal/pipeline/bankbasis.go:209-215`), то есть уже `TrimSpace`-нутым и к пробелам нечувствительным. ⇒ для трёх
файлов, где комментарии были (`classifier`, `terminologist`, `editor-mono`), неаккуратная уборка
сдвинула бы хеш, но денег бы НЕ стоила; денежной она была бы у стадийных промтов и ремонта, а в них
комментариев не было. Ведущие пустые строки в трёх файлах оставлены как гигиена, а не как экономия.
- **Правка ТЕКСТА банк-роли — деньги.** `internal/pipeline/bankbasis.go:183-187` хеширует
ОТРЕНДЕРЕННЫЙ системный текст терминолога И классификатора в личность продюсера: решённые термины
перестают отдаваться из базиса и пере-спрашиваются из ПОЖИЗНЕННОГО бюджета роли. На английской
книге сегодня $0 — банка нет.
- **Любой файл в `configs/langpacks/en-ru/` делает незапускаемой английскую книгу, ОБЪЯВИВШУЮ
`langpack_root`** — и вот условие, при котором вывод перестаёт держаться: книга без этого ключа
выходит раньше (`internal/pipeline/runner.go:432`, `if r.Book.LangpackRoot == "" { return nil }`) и
каталога не видит вовсе. Дальше загрузчик включается по существованию каталога ПАРЫ
(`runner.go:436`) и затем требует у источника `en` либо одиннадцать таблиц КИТАЙСКОЙ морфологии, либо
пиньинь. Замер живым движком: с
созданным `langpacks/en-ru/` (один `heading.txt`) `tmctl translate` падает
`open …/langpacks/en/surnames-single.txt: no such file`, EXIT=1. ⇒ `heading.txt` («Глава {n}») и
`terminology.txt` (ширины KWIC) завести НЕЛЬЗЯ до строки бэклога 347.
## 7. Чего пара НЕ может предъявить
**Качество этих данных не измерено и в этом заходе измерено не будет.** Размеченного корпуса для
en→ru в репозитории нет, человеческого эталона книги в дереве нет. Батарея зелена при любом их
содержании. Единственный рубеж — консилиум: сессия плюс советчик Fable 5, шесть кругов.
⚠ И у этого рубежа названа своя граница: обе стороны читают один текст одними приборами, поэтому дефект,
невидимый обоим, консилиум не ловит — в этом паке его дважды поймали ТРЕТЬИ читатели (§10).
## 8. Провенанс, переехавший из промтов
Из `prompts/zh-ru/` комментарии сняты по слову владельца. ⚠ **Здесь — то, что несло ВЫВОД, а не весь их
текст:** перечни нот, цифры и примеры из снятых комментариев сюда не переносились (замер веером: из
19 опрошенных носителей до этого раздела доехали 6). Полный прежний текст берётся одной командой —
`git show HEAD:backend/prompts/zh-ru/<файл>`, и это адрес, а не обещание.
- `terminologist.md`: третье поле ответа — уверенность 0100, **НЕ арбитр**, только порядок ручного
ревью; ни весов, ни порогов, ни сравнения между моделями (`D39.102`; разбор поля — `internal/terminology/terminology.go:710-738`).
- `classifier.md`: третье поле — род; производителя у него не было вовсе (терминолог его не
возвращает, майнер брал из сида, на несидированной книге ось мертва), и этот проход читает те же
контексты по тем же кандидатам ⇒ род стоит ноль новых вызовов. Значение вне словаря
`male|female|neuter|none` движок считает и ОТБРАСЫВАЕТ, класс при этом сохраняется. `hidden` в
словарь не входит НАМЕРЕННО: это решение о спойлерной политике книги, а не наблюдение по
контекстам, и остаётся ручным, в сиде (`internal/terminology/classify.go:42`).
- `editor-mono.md`: **СПИТ.** Монолингвальный вариант редактора (D30.1, подтверждающий арм D13.1).
Проверено исполнением: живых ключей, ссылающихся на него, — **0** (все 17 упоминаний в
`books/` и `configs/` суть комментарии). Файл держит пин `TestEditorPromptIsBilingual`
(`internal/pipeline/runner_memory_test.go:320`, утверждение про `{{text}}``:346`): файл обязан
существовать у своей пары и НЕ ссылаться на `{{text}}`. ⚠ Пин пере-написан этим паком на цикл по
парам, `editor-mono` опционален на пару — прежний адрес `:331` после правки указывает в другую строку.
## 9. Оговорки советчика, взятые дословно
- Текст рамки двух браков — ВТОРАЯ редакция `_BRAK`, и это **доказано кодом, а не предположено**
(пере-снято полигонной сессией, сверено мной): в `eval/dovodka/rol.py` два присваивания ОДНОМУ имени —
`:192` и `:214` — при единственном использовании на `:226`; в Python побеждает последнее, значит первая
редакция мертва. Коммит `3075566` («Freeze two ablation variants») добавил ОБА определения сразу ⇒ первая
была мёртвым кодом с рождения, а не устаревшей версией.
⚠ Артефактами это недоказуемо: провод в клетках полигона не хранится. Пере-снято на правильной
популяции — в `books/dovodka` **1345** JSON и 82 txt (прежняя редакция печатала 5161, это сумма по
ЧЕТЫРЁМ корпусам `books/`, то есть контроль был снят не с того предмета). Ключей `prompt`, `system`,
`messages`**0 файлов**; фраза рамки «БРАКУЮТСЯ ДВА ИСХОДА» — **0**. Контроль прибора на той же
выдаче: ключ `uid` найден в **1235** файлах ⇒ читались существующие клетки. Доказательство редакции
`_BRAK` поэтому механическое, по коду.
- `judge-selector.md`: «выведи ТОЛЬКО номер» — **заглушка до парсера фазы 2**. Парсера ответа судьи в
движке нет, роль отказывается фазой 0; когда формат определит движок, промт подгоняется под него.
- Вес промта (§2) измерен на жильце `gemini-3.1-flash-lite` при вендор-дефолте (`eval/dovodka/rol.py:630`), не на
боевом редакторе. Переносится НАПРАВЛЕНИЕ, не величина.
## 10. Как собран пакет
Шесть кругов консилиума сессии и советчика Fable 5 (заказ владельца 17.09: «пока вы оба не окните»).
Круг 1 — инвентарь механизмов английского. Круг 2 — 19 механизмов, вердикты по каждому правилу,
7 дефектов китайского промтинга. Круг 3 — язык инструкций (решение владельца: русский), три
китайских комментария, разбор пере-писанного пакета. Круг 4 — шесть правок переводчику, три
редактору, одна классификатору. Круг 5 — одна висящая ссылка и пере-формулировка признака
стихотворного блока после того, как полигон отозвал свой же совет. Круг 6 — пакет целиком под
ратифицированным правилом владельца «данные обязаны так же хорошо отрабатывать там, где явления нет»:
подтверждены чистыми семь правил, найдены НЕ чистыми два (обоснование «в мире книги метрической системы
нет» — факт этой книги; признак стихотворного блока ловил оборванные реплики) и досчитаны примеры
законодателей с 18 до 25. Восемь ОК с обеих сторон.
**И то, чего консилиум НЕ даёт:** обе стороны читают один и тот же текст одними и теми же приборами.
Дефект, невидимый обоим, консилиум не ловит — в этом паке его дважды ловил ТРЕТИЙ читатель (оркестратор
№23: утечка конкретной книги в пар-слой; полигонная сессия: отозванный механизм ингеста).
## 11. Три заказанных замера — результаты (§4.5, §4.6, §4.7 промта пака)
Все три сняты кодом САМОГО движка на корпусе из 75 русских переводов ЭТОЙ книги
(`books/role-topology/en2-*.json`, 15 исходных чанков × 5 армов), контроли напечатаны рядом.
**§4.5 ПОРОГ ЭХА — НЕ ДВИГАТЬ.** Функция `sourceScriptShare` с `lang.LangScripts("en")`, порог
`cjkEchoThreshold = 0.15`:
| | |
|---|---|
| доля латиницы в русском выходе | min 0.0000 · медиана 0.0000 · max 0.1211 |
| выше порога 0.15 | **0 единиц из 75** |
| по 14 прозаическим чанкам из 15 | максимум **0.0136** (запас к порогу 11×) |
| единственный чанк у порога | 0.1211 — выходные данные издательства, не проза (запас 1.24×) |
| контроль прибора | чистый английский выход = **0.8095**, то есть прибор различает эхо |
| контроль данных | `len(LangScripts("en")) = 1` — детектор не мерит пустоту |
⇒ порог трогать нечем: ложных срабатываний на прозе нет, а пере-снапшот стоил бы движения
`classifierVersion` (подстрока `echo015`). Ряд бэклога **260** этим замером не закрыт: он про
латиницу-тяжёлый русский на ДРУГОЙ паре, и мой корпус его клетку не покрывает.
**§4.6 ДЕТЕКТОР ЛАТИНИЦЫ — порог не трогать, лечение аллоу-листом, и у него найден свой дефект.**
Боевой путь `checks.RunCheapGates` с чекерами цели `ru` (контроли: `TargetActive()=true`,
`TargetScriptNonLatin()=true`, подставная утечка «something» ловится):
- сработало **10 единиц из 75**, и по прозе хиты ровно двух родов: французские вставки, которые
правило пары велит СОХРАНЯТЬ (`oui` ×3, `gens` ×2, `armes` ×2), и собственное слово книги
(`Vampyr`/`VAMPYR`);
- остальные хиты — выходные данные издательства на служебной странице (`macmillan`,
`newslettersignup`, `jaykristoff`, `New`, `York`, `Times`, `Today`, `USA`, `Aurealis`, `Publishing`,
`Group`, `Martin`), то есть не проза.
**И дефект самого детектора, найденный по ходу:** `isLatinLetterOrDigit` (`internal/checks/checkers.go:596`) —
только ASCII, поэтому акцентированное слово дробится на осколки. Замер функцией движка:
`Aveléne` → хит **`Avel`**; `Château Impérial`**ТРИ** хита `Imp`, `rial`, `teau`. ⇒ штатное лечение
аллоу-листом (`book.yaml style_allowlist`) на акцентированных именах НЕ РАБОТАЕТ: в список пришлось бы
вписывать `teau`. Передано оркестратору рядом; на этой паре экспозиция низкая, потому что правило пары
велит имена транслитерировать.
**§4.7 КЛЮЧИ БАНКА НА ЛАТИНИЦЕ — подозрение подтверждено и сужено.** Прогон нормализатором движка
(`text.NormalizeSourceKey`) по 25 поверхностям реально предложенного банка этой книги
(`books/role-topology/bank-en.json`; контроль: 25 из 25 встречаются в книге):
| | |
|---|---|
| ключей со строчным омографом | **3 из 25**: `Dead` 167/143 · `Empire` 11/84 · `Marquis` 155/2 |
| чистых (личные имена) | 22 |
| `collisionProneKey` | false у всех — все ≥4 значащих рун, короткий-ключевой порог не при чём |
| шире по книге, тот же класс | `Silver` 19/177 · `Will` 19/288 · `Faithful` 18/51 · `Dawn` 17/61 · `Forever` 209/76; чисты `Grail` 293/0 и `Almighty` 57/0 |
⇒ дефект реален и узок: он бьёт по заглавным НАРИЦАТЕЛЬНЫМ (фракция «the Dead», держава «the Empire»),
а не по личным именам. Лечение БЕЗ правки Go — дисциплина промтов: банкнота и терминолог этой пары
велят не канонизировать одиночное заглавное нарицательное. Правка `norm.go` передана оркестратору
рядом, с ценой: `bankRoleProducer` делает правку текста банк-роли пере-спросом решённых терминов.
## 12. Нарезка глав (§4.4) — что решено и почему
Замер прибором самого движка (`--json`-манифест, 71 запись; сумма `price.source_chars` = 1 601 395
совпадает со сводкой): глав **71**, `heading` НЕПУСТ у **0** из 71, `title_raw`у 71 из 71;
крупных (≥100 тыс. знаков) — **6** (BOOK TWO 392 312 … BOOK SIX 140 022); крошечных (<200) **64**;
средних — **1**.
⛔ 64 крошечные главы — НЕ «страницы-разделители»: это **разрезанная на файлы страница «Contents»**.
Замер по EPUB напрямую: navPoint'ов 71, с фрагментом `#`**ноль**, целей-документов 71 различных;
`index_split_058.html` (583 руны) начинается словами «Contents · Title Page … Chapter I. Parting Gifts»
и обрывается на 25-й записи, дальше каждая запись оглавления лежит СВОИМ документом. Настоящие главы
в TOC не названы вовсе: проза живёт в документах между титулами книг, navPoint'а у них нет, и
`groupChapters` приписывает их к предыдущей главе. Заголовок главы ВНУТРИ документа есть, и его форма
английская — голая РИМСКАЯ цифра отдельной строкой, под ней название капсом; `<h1>``<h6>` нет ни одного.
**РЕШЕНИЕ: нарезку EPUB в этом паке НЕ чиню.** (1) Эксперимент она не блокирует: BOOK THREE = 148 782
знака без пробелов ≈ **40 874 токена** при цели 41 027, промах 0.37 %. (2) Починка требует не порога в
данных, а нового рода грамматики (безмаркерный заголовок + римские цифры) и разреза ВНУТРИ документа —
это тело ряда **283**. (3) Порог «сколько знаков делает главу главой» проблему не решает: он убрал бы
64 крошечные записи и оставил шесть полотен.
**Граница ряда 303 названа явно:** не-CJK TXT этим паком НЕ починен. Пере-снято с контролем на второй
английской книге: `books/fifty_shades_1_en.txt`, строк `^[[:space:]]*Chapter `**26**, а
`tmctl manifest` даёт **chapters=1 units=146 chunks=147** и без `en/structure.txt`, и с файлом
`marker⇥Chapter`. ⚠ `chapter structure: none` в обоих прогонах НЕ доказывает, что файл не прочли:
`structureOf` (`internal/chunk/ingest.go:169-171`) отдаёт `none` при `len(chapters) < 2` независимо от
происхождения грамматики.
**§4.8 бюджет нарезки** — оценка заведена в `configs/pairs/en-ru.yaml`, названа оценкой (см. §2 того файла).

View file

@ -29,9 +29,9 @@ ceilings:
day_usd: 2.00
# --- Демонстрация pair-сеама (D39 слой 2), НЕ активная конфигурация ------------------
# Промпт стадии резолвится КОНВЕНЦИЕЙ prompts/<пара>/<роль>.md (пак-15): в репо есть только
# prompts/zh-ru/ (conventions написаны под китайский
# исходник). Книга с source_lang: ja даёт LangPair() = "ja-ru", которого в паке нет, и
# Промпт стадии резолвится КОНВЕНЦИЕЙ prompts/<пара>/<роль>.md (пак-15): в репо лежат паки
# prompts/zh-ru/ и prompts/en-ru/, каждый писан под свой источник. Книга с source_lang: ja даёт
# LangPair() = "ja-ru", которого в паке нет, и
# загрузка ПАДАЕТ ГРОМКО в LoadPipeline (никогда молча не подставляем чужую пару). Чтобы
# увидеть сеам, поменяйте выше на блок ниже (ja-фрагмент лежит в chapter1-chunk0.txt):
# source_lang: ja

View file

@ -73,9 +73,12 @@ func (c InternalCall) Stage() Stage {
// the block cannot reserve the buffer, so a call reaching this seam is never on an additive provider.
// A future gate that wants an additive model must add the key AND lift that refusal together.
//
// PromptOverride / PromptPath / PromptVersion: internal calls arrive with their messages ALREADY
// rendered (each gate resolves and versions its own prompt), so the stage template machinery is not
// consulted for them.
// PromptOverride / PromptVariant / PromptPath / PromptVersion: internal calls arrive with their
// messages ALREADY rendered (each gate resolves and versions its own prompt), so the stage template
// machinery is not consulted for them. PromptVariant is on this list for the same reason as the
// other three and not because variants are unwanted: the gates that make internal calls resolve
// their prompts by the plain role convention themselves (gates.terminology.PromptPath /
// ClassifyPromptPath), so a variant selected HERE would name a file nobody reads.
//
// EscalateTo / ResolvedHop: no escalation hop. maybeEscalate is reached from runStage, not from the
// internal-call paths, and these calls degrade by leaving their work unchanged rather than by paying

View file

@ -27,6 +27,7 @@ func TestInternalCallDecidesEveryStageField(t *testing.T) {
"Temperature": "structured replies the engine re-parses; sampling variance is pure risk",
"ReasoningMaxTokens": "gates carry no such key, and the loader refuses additive-billing models for them",
"PromptOverride": "internal calls arrive with messages already rendered",
"PromptVariant": "same — a gate resolves its own prompt by the plain role convention",
"PromptPath": "same",
"PromptVersion": "same — each gate versions its own prompt",
"EscalateTo": "no hop; these calls degrade by leaving their work unchanged",

View file

@ -0,0 +1,62 @@
package config
import (
"path/filepath"
"testing"
)
// TestEnRuPairLayerCarriesItsOwnFertilityEstimate: the generic segmentation fallback is the zh-ru
// calibration (LoadPipeline says so), so a pair file omitting `fertility.other` chunks an English book
// on Chinese numbers. The premise — estimate != fallback — is asserted, or the fixture measures nothing.
func TestEnRuPairLayerCarriesItsOwnFertilityEstimate(t *testing.T) {
const genericFallbackOther = 0.3852 // the literal LoadPipeline falls back to (zh-ru calibration)
const enRuEstimateOther = 0.4203 // r/3.64, DeepSeek r=1.53 — an ESTIMATE, see the pair file
if enRuEstimateOther == genericFallbackOther {
t.Fatalf("premise broken: the en-ru estimate equals the generic fallback (%v), so nothing below can distinguish the pair layer from the default", enRuEstimateOther)
}
models, err := LoadModels(filepath.Join("..", "..", "configs", "models.yaml"))
if err != nil {
t.Fatalf("load models: %v", err)
}
cfg := filepath.Join("..", "..", "configs", "pipeline-c1.yaml")
en, err := LoadPipeline(cfg, models, "en-ru", nil)
if err != nil {
t.Fatalf("pipeline-c1 must load for en-ru: %v", err)
}
if got := en.Segmentation.Fertility.Other; got != enRuEstimateOther {
t.Errorf("en-ru fertility.other = %v, want %v from configs/pairs/en-ru.yaml — %v would mean the pair file is not read and the English book is being chunked on the zh-ru calibration",
got, enRuEstimateOther, genericFallbackOther)
}
// cjk is deliberately UNSTATED for en-ru (dense runes measured 0 in 351 of 351 units of the live
// book, so the coefficient multiplies zero); the fallback is therefore expected, and asserting it
// keeps "unstated" from drifting into "stated by accident".
if got := en.Segmentation.Fertility.CJK; got != 1.1978 {
t.Errorf("en-ru fertility.cjk = %v, want the fallback 1.1978 — the pair file states no cjk value on purpose", got)
}
// The budgets are target-side (ru OUTPUT tokens), so en-ru keeps the fallback deliberately too.
if en.Segmentation.DraftBudgetOut != 1797 || en.Segmentation.EditCeilingOut != 3200 {
t.Errorf("en-ru budgets = %d/%d, want the fallback 1797/3200 (they are measured in ru output tokens, a target-side quantity)",
en.Segmentation.DraftBudgetOut, en.Segmentation.EditCeilingOut)
}
if got := en.Gates.Coverage.LenRatio["en-ru"]; len(got) != 2 || got[0] != 0.70 || got[1] != 1.4 {
t.Errorf("en-ru corridor = %v, want [0.7 1.4] from the pair layer", got)
}
// And the other direction: the zh-ru pair must be untouched by the English file existing.
zh, err := LoadPipeline(cfg, models, "zh-ru", nil)
if err != nil {
t.Fatalf("pipeline-c1 must load for zh-ru: %v", err)
}
// ⚠ The zh fertility numbers are simultaneously the pair file's literals AND the generic fallback, so
// they cannot tell "the production pair layer is read" from "the production pair file is gone" — they only
// say the English file did not move them. The corridor is what discriminates: [2.2 4.2] exists nowhere
// but configs/pairs/zh-ru.yaml, so its absence would mean the zh pair layer stopped being read.
if zh.Segmentation.Fertility.Other != genericFallbackOther || zh.Segmentation.Fertility.CJK != 1.1978 {
t.Errorf("zh-ru fertility = %v/%v, want 1.1978/%v — the English pair file must not move the production pair",
zh.Segmentation.Fertility.CJK, zh.Segmentation.Fertility.Other, genericFallbackOther)
}
if got := zh.Gates.Coverage.LenRatio["zh-ru"]; len(got) != 2 || got[0] != 2.2 || got[1] != 4.2 {
t.Errorf("zh-ru corridor = %v, want [2.2 4.2] from configs/pairs/zh-ru.yaml — this is the one value of the production pair layer that is NOT also the engine fallback, so it is what proves the layer is still read", got)
}
}

View file

@ -227,6 +227,13 @@ type Stage struct {
// (never its path) and the book's pair rides BriefHash (source_lang/target_lang), so a pair or
// content change is a loud --resnapshot while a moved file is not.
PromptOverride string `yaml:"prompt_override"`
// PromptVariant names a VARIANT of the role and resolves by the SAME convention:
// `<prompts root>/<pair>/<role>-<variant>.md`. It exists because PromptOverride names a PATH, and a
// path in a shipping config fixes the pair the book did not choose — the latent "a book rides another
// pair's conventions" bug, which only stayed unreachable while one prompt pack existed.
// PromptOverride keeps its ratified pair-agnostic meaning (TestPromptOverrideIsPairAgnostic) for
// fixtures and one-off arms; the two keys are mutually exclusive and the value is a bare name.
PromptVariant string `yaml:"prompt_variant"`
// LegacyPrompt / LegacyPrompts are the RETIRED pre-pack-15 keys, kept in the schema for exactly one
// reason: to be REJECTED. yaml.v3 ignores unknown fields, so a config still carrying `prompt:` or
// `prompts: {zh-ru: …}` — or a typo like `promt_override:` — would parse to an EMPTY override and the
@ -541,9 +548,16 @@ type Fanout struct {
}
// promptConventionPath is the convention that replaced the per-config pair→path listing: a stage's
// prompt is the file named after its ROLE inside its PAIR's directory.
func promptConventionPath(promptsRoot, pair, role string) string {
return filepath.Join(promptsRoot, pair, role+".md")
// prompt is the file named after its ROLE inside its PAIR's directory. A non-empty variant names a
// NAMED VARIANT of that role — `<role>-<variant>.md` in the same directory — so the pair binding is
// identical for both forms. ONE spelling of the convention on purpose: a second helper would be a
// second carrier, and the two would be free to disagree about where a prompt lives.
func promptConventionPath(promptsRoot, pair, role, variant string) string {
name := role
if variant != "" {
name = role + "-" + variant
}
return filepath.Join(promptsRoot, pair, name+".md")
}
// CheckRunnable rejects a config whose mechanics the Phase-0 runner does NOT
@ -894,9 +908,9 @@ func (p *Pipeline) gateModels() []string {
// book of the given language pair ("zh-ru") and content-label set. The pair selects two things at load
// time: the PAIR LAYER (`<config dir>/pairs/<pair>.yaml` — segmentation calibration and the excision
// corridor, optional) and each stage's PROMPT, resolved by convention as
// `<prompts root>/<pair>/<role>.md` unless the stage sets prompt_override. A missing convention prompt
// is a LOUD failure naming the path: a book whose pair has no prompt pack must stop, never silently run
// another pair's conventions.
// `<prompts root>/<pair>/<role>.md` (or `<role>-<variant>.md` with prompt_variant) unless the stage sets
// prompt_override. A missing convention prompt is a LOUD failure naming the path: a book whose pair has no
// prompt pack must stop, never silently run another pair's conventions.
//
// The labels arrive through the same seam as the pair, and for the same reason: they are book data the
// config must be resolved AGAINST, and resolving them here keeps the loader's one-error-list doctrine
@ -1044,14 +1058,28 @@ func LoadPipeline(path string, models *Models, pair string, labels []string) (*P
}
// A retired prompt key is a MIGRATION error, never a silently ignored one (see Stage.LegacyPrompt).
if st.LegacyPrompt != "" {
bad("stage %q: `prompt:` is retired — a stage's prompt is resolved by convention as <prompts root>/<pair>/<role>.md (pack-15). Move the file there, or set `prompt_override:` if this stage deliberately runs a variant", st.Name)
bad("stage %q: `prompt:` is retired — a stage's prompt is resolved by convention as <prompts root>/<pair>/<role>.md (pack-15). Move the file there; a deliberate VARIANT of the role is `prompt_variant:`, and an off-convention PATH is `prompt_override:`", st.Name)
}
if len(st.LegacyPrompts) > 0 {
bad("stage %q: the pair-keyed `prompts:` map is retired — a stage's prompt is resolved by convention as <prompts root>/<pair>/<role>.md (pack-15), so a pair is a DIRECTORY, not a config entry. Drop the map (put the pair calibration in configs/pairs/<pair>.yaml); set `prompt_override:` only if this stage deliberately runs a variant", st.Name)
bad("stage %q: the pair-keyed `prompts:` map is retired — a stage's prompt is resolved by convention as <prompts root>/<pair>/<role>.md (pack-15), so a pair is a DIRECTORY, not a config entry. Drop the map (put the pair calibration in configs/pairs/<pair>.yaml); a deliberate VARIANT of the role is `prompt_variant:`, an off-convention PATH is `prompt_override:`", st.Name)
}
// Prompt resolution: the deliberate override, else the pair/role convention. The convention
// path must EXIST — a missing file is the "this pair has no prompt pack" case, and it stops the
// load naming the path it looked for.
// Two ways to say "not the plain role prompt" state two different intents, so setting both is a
// config error rather than a precedence puzzle: the reader of such a file cannot tell which one
// the author meant, and a precedence rule would make the loser silently dead.
if st.PromptOverride != "" && st.PromptVariant != "" {
bad("stage %q: prompt_override and prompt_variant are mutually exclusive — `prompt_variant: %s` names a variant INSIDE the book's pair (<prompts root>/<pair>/%s-%s.md), while `prompt_override` names an explicit path and fixes the pair; keep the one you mean",
st.Name, st.PromptVariant, st.Role, st.PromptVariant)
}
// A variant is a bare NAME. A separator or `..` would make this key a path — which is what
// prompt_override is for — and would smuggle back the very pair-fixing this key exists to end.
if v := st.PromptVariant; v != "" {
if strings.ContainsAny(v, `/\`) || strings.Contains(v, "..") || strings.TrimSpace(v) != v {
bad("stage %q: prompt_variant %q must be a bare name (no path separators, no `..`, no surrounding spaces) — it is joined as <prompts root>/<pair>/%s-<variant>.md; use prompt_override for an explicit path", st.Name, v, st.Role)
}
}
switch {
case st.PromptOverride != "":
p.Stages[i].PromptPath = resolvePrompt(st.PromptOverride)
@ -1060,10 +1088,17 @@ func LoadPipeline(path string, models *Models, pair string, labels []string) (*P
case pair == "":
bad("stage %q: no language pair to resolve a prompt for — the book must declare source_lang/target_lang, or the stage must set prompt_override", st.Name)
default:
cp := promptConventionPath(promptsRoot, pair, st.Role)
cp := promptConventionPath(promptsRoot, pair, st.Role, st.PromptVariant)
if _, serr := os.Stat(cp); serr != nil {
bad("stage %q: no prompt for pair %q role %q — expected %s (conventions authored in the right language). Add that file, or set prompt_override; never silently substitute another pair's conventions (D39 layer 2)",
st.Name, pair, st.Role, cp)
what, cure := fmt.Sprintf("role %q", st.Role), "Add that file, or set prompt_override for a deliberate off-convention path"
if st.PromptVariant != "" {
// The cure differs by case: a missing VARIANT is a typo or an unwritten file, and
// prompt_override here would answer it by naming a path — pinning a pair the book never chose.
what = fmt.Sprintf("role %q variant %q", st.Role, st.PromptVariant)
cure = "Add that file, or fix the prompt_variant name — a variant resolves inside the book's OWN pair"
}
bad("stage %q: no prompt for pair %q %s — expected %s (conventions authored in the right language). %s; never silently substitute another pair's conventions (D39 layer 2)",
st.Name, pair, what, cp, cure)
}
p.Stages[i].PromptPath = cp
}
@ -1222,7 +1257,7 @@ func LoadPipeline(path string, models *Models, pair string, labels []string) (*P
if pair == "" {
bad("gates.terminology is enabled but the book declares no language pair — the role prompt is resolved as <prompts root>/<pair>/terminologist.md")
} else {
p.Gates.Terminology.PromptPath = promptConventionPath(promptsRoot, pair, terminologyRoleName)
p.Gates.Terminology.PromptPath = promptConventionPath(promptsRoot, pair, terminologyRoleName, "")
if _, err := os.Stat(p.Gates.Terminology.PromptPath); err != nil {
bad("gates.terminology is enabled but its role prompt is missing — expected %s (authored in the pair's own language, like the other role prompts)", p.Gates.Terminology.PromptPath)
}
@ -1240,7 +1275,7 @@ func LoadPipeline(path string, models *Models, pair string, labels []string) (*P
bad("gates.terminology.classify_budget_usd must be > 0 when classify_types is on (a phase that can never spend is a silent no-op)")
}
if pair != "" {
p.Gates.Terminology.ClassifyPromptPath = promptConventionPath(promptsRoot, pair, classifierRoleName)
p.Gates.Terminology.ClassifyPromptPath = promptConventionPath(promptsRoot, pair, classifierRoleName, "")
if _, err := os.Stat(p.Gates.Terminology.ClassifyPromptPath); err != nil {
bad("gates.terminology.classify_types is on but its prompt is missing — expected %s (authored in the pair's own language, like the other role prompts)", p.Gates.Terminology.ClassifyPromptPath)
}

View file

@ -142,11 +142,12 @@ func TestPromptOverrideMustBeReadable(t *testing.T) {
// TestBoevoyPipelineC1ResolvesZhRu proves the migrated prod config resolves its prompts BY PAIR for zh→ru,
// and that a pair with no pack fails loud.
//
// ⚠ The draft stage is the ONE deliberate exception to the role convention and has been since backlog row
// 140 turned the bank contour on: the banknote channel is two coordinated changes, and the second is the
// prompt that instructs the model to emit the ⟦TM-BANK-v1⟧ block (D39.10). It is declared with
// `prompt_override`, which is exactly what the convention keeps that key for. The assertion follows the
// override rather than being dropped, so an override that appears somewhere nobody meant it still reds.
// ⚠ The draft stage runs a named VARIANT of its role, and has since backlog row 140 turned the bank contour
// on: the banknote channel is two coordinated changes, and the second is the prompt that instructs the model
// to emit the ⟦TM-BANK-v1⟧ block (D39.10). It is declared with `prompt_variant: banknote`, which resolves
// inside the BOOK's pair; `prompt_override` would name a path and so pin a pair the book never chose. The
// assertion follows the variant rather than being dropped, so a variant appearing somewhere nobody meant it
// still reds.
func TestBoevoyPipelineC1ResolvesZhRu(t *testing.T) {
models, err := LoadModels(filepath.Join("..", "..", "configs", "models.yaml"))
if err != nil {
@ -157,10 +158,10 @@ func TestBoevoyPipelineC1ResolvesZhRu(t *testing.T) {
if err != nil {
t.Fatalf("load pipeline-c1: %v", err)
}
overrides := map[string]string{"draft": "translator-banknote.md"}
variants := map[string]string{"draft": "translator-banknote.md"}
for _, st := range p.Stages {
file := st.Role + ".md"
if o, ok := overrides[st.Name]; ok {
if o, ok := variants[st.Name]; ok {
file = o
}
want := filepath.Join("prompts", "zh-ru", file)
@ -173,7 +174,7 @@ func TestBoevoyPipelineC1ResolvesZhRu(t *testing.T) {
}
// A book of a pair the repo does not carry must fail loud (closes the ja-runs-zh-prompt bug).
if _, err := LoadPipeline(cfg, models, "ja-ru", nil); err == nil {
t.Error("pipeline-c1 must fail loud for ja-ru (only prompts/zh-ru exists)")
t.Error("pipeline-c1 must fail loud for ja-ru: this repo carries no prompts/ja-ru pack")
}
}

View file

@ -0,0 +1,147 @@
package config
import (
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
// A named variant resolves from the BOOK's pair: `<prompts root>/<pair>/<role>-<variant>.md`.
func TestPromptVariantResolvesInsideTheBooksPair(t *testing.T) {
dir := promptProject(t, "zh-ru", "en-ru")
for _, pair := range []string{"zh-ru", "en-ru"} {
writeTmp(t, filepath.Join(dir, "prompts", pair, "translator-banknote.md"), "sys "+pair+"\n---USER---\n{{text}}")
}
cfg := draftOnlyConfig(t, dir, " prompt_variant: banknote\n")
for _, pair := range []string{"zh-ru", "en-ru"} {
p, err := LoadPipeline(cfg, miniModels(t), pair, nil)
if err != nil {
t.Fatalf("pair %q must load: %v", pair, err)
}
want := filepath.Join(dir, "prompts", pair, "translator-banknote.md")
if p.Stages[0].PromptPath != want {
t.Errorf("pair %q resolved %q, want %q", pair, p.Stages[0].PromptPath, want)
}
}
}
// A missing variant stops the load naming pair, role and variant; a fall-through to the plain role
// prompt would be a stage running a prompt nobody chose.
func TestPromptVariantMissingFileFailsLoudNamingPairAndVariant(t *testing.T) {
dir := promptProject(t, "zh-ru")
cfg := draftOnlyConfig(t, dir, " prompt_variant: nosuch\n")
_, err := LoadPipeline(cfg, miniModels(t), "zh-ru", nil)
if err == nil {
t.Fatal("a missing variant file must fail loud")
}
// `variant %q` verbatim, not the bare token: "nosuch" is also a substring of the PRINTED PATH, so the
// variant half of this assertion would be satisfied by the path even if the message never named a variant.
for _, want := range []string{"zh-ru", "translator", `variant "nosuch"`, filepath.Join("prompts", "zh-ru", "translator-nosuch.md")} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error must name %q, got: %v", want, err)
}
}
}
// Both keys set is a config error, not a precedence puzzle: precedence would make the loser silently dead.
func TestPromptVariantAndOverrideAreMutuallyExclusive(t *testing.T) {
dir := promptProject(t, "zh-ru")
writeTmp(t, filepath.Join(dir, "prompts", "zh-ru", "translator-banknote.md"), "sys\n---USER---\n{{text}}")
own := writeTmp(t, filepath.Join(dir, "own.md"), "sys\n---USER---\n{{text}}")
cfg := draftOnlyConfig(t, dir, " prompt_variant: banknote\n prompt_override: "+own+"\n")
_, err := LoadPipeline(cfg, miniModels(t), "zh-ru", nil)
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
t.Errorf("setting both keys must fail loud, got: %v", err)
}
}
// A separator or `..` would make this key a path — which is what prompt_override is for.
func TestPromptVariantMustBeABareName(t *testing.T) {
dir := promptProject(t, "zh-ru")
for _, bad := range []string{"../zh-ru/translator-banknote", "zh-ru/banknote", `sub\banknote`, " banknote"} {
cfg := draftOnlyConfig(t, dir, " prompt_variant: '"+bad+"'\n")
_, err := LoadPipeline(cfg, miniModels(t), "zh-ru", nil)
if err == nil || !strings.Contains(err.Error(), "bare name") {
t.Errorf("prompt_variant %q must be refused as not-a-bare-name, got: %v", bad, err)
}
}
}
// TestEveryShippingConfigResolvesInsideItsOwnPair: no shipping config may resolve a stage prompt outside
// the book's own pair directory. Until a second prompt pack existed the property was held by ABSENCE —
// other pairs were refused on the stages whose files were missing — so it was never asserted.
//
// ⛔ THE PREDICATE IS THE PARENT DIRECTORY, NOT A SUBSTRING, and the accounting is per (config, pair).
// A `strings.Contains(path, "/en-ru/")` test passes on `…/prompts/en-ru/../zh-ru/translator.md`, and a
// per-PAIR counter cannot tell three configs from four — a config that stops loading removes itself from
// the gate in silence. Both were live holes in the first version of this pin.
func TestEveryShippingConfigResolvesInsideItsOwnPair(t *testing.T) {
configsDir := filepath.Join("..", "..", "configs")
promptsDir := filepath.Join("..", "..", "prompts")
models, err := LoadModels(filepath.Join(configsDir, "models.yaml"))
if err != nil {
t.Fatalf("load models: %v", err)
}
cfgs, err := filepath.Glob(filepath.Join(configsDir, "pipeline-*.yaml"))
if err != nil {
t.Fatal(err)
}
sort.Strings(cfgs)
if len(cfgs) < 4 {
t.Fatalf("premise broken: found %d shipping configs in %s, expected at least the four this repo ships", len(cfgs), configsDir)
}
ents, err := os.ReadDir(promptsDir)
if err != nil {
t.Fatal(err)
}
var pairs []string
for _, e := range ents {
if e.IsDir() {
pairs = append(pairs, e.Name())
}
}
sort.Strings(pairs)
if len(pairs) < 2 {
t.Skipf("only %d prompt pack(s) in %s — the cross-pair property needs two to be observable", len(pairs), promptsDir)
}
// inPairDir answers the question the pin is about: is this file's PARENT the pair's directory. The path
// is cleaned first, so a `..` segment cannot smuggle another pair's file past a substring match.
inPairDir := func(p, pair string) bool {
return filepath.Base(filepath.Dir(filepath.Clean(p))) == pair
}
checked := 0
for _, cfg := range cfgs {
for _, pair := range pairs {
p, lerr := LoadPipeline(cfg, models, pair, nil)
if lerr != nil {
// NOT skipped: a shipping config that cannot serve a pair whose pack this repo carries is
// either a half-landed pack or a config that quietly left the gate. Both must be seen.
t.Errorf("%s + pair %q: does not load (%v) — a shipping config must serve every pair whose prompt pack this repo carries, or this gate stops covering it in silence",
filepath.Base(cfg), pair, lerr)
continue
}
checked++
for _, st := range p.Stages {
if !inPairDir(st.PromptPath, pair) {
t.Errorf("%s + pair %q: stage %q (role %q) resolved %q — a prompt OUTSIDE the book's own pair directory %q; a shipping config must never hand a book another pair's conventions",
filepath.Base(cfg), pair, st.Name, st.Role, st.PromptPath, pair)
}
}
for _, g := range []struct{ what, path string }{
{"gates.terminology", p.Gates.Terminology.PromptPath},
{"gates.terminology classifier", p.Gates.Terminology.ClassifyPromptPath},
} {
if g.path != "" && !inPairDir(g.path, pair) {
t.Errorf("%s + pair %q: %s prompt resolved %q — outside the pair directory %q", filepath.Base(cfg), pair, g.what, g.path, pair)
}
}
}
}
if want := len(cfgs) * len(pairs); checked != want {
t.Errorf("checked %d of %d (config, pair) combinations — the shortfall is reported above; a silent one would read as full coverage", checked, want)
}
t.Logf("checked %d (config, pair) combinations: %d configs × %d pairs", checked, len(cfgs), len(pairs))
}

View file

@ -0,0 +1,81 @@
package pipeline
import (
"os"
"path/filepath"
"strings"
"testing"
)
// banknotebody_test.go: a pair's banknote prompt must be that pair's translator prompt plus ONE inserted
// block, and nothing else.
//
// WHY A GATE AND NOT A NOTE. Both files ship and both are labelled, so the byte ledger
// (promptlabel_test.go) sees any edit to either — until the ledger is re-written through its own documented
// update path, which is what a deliberate prompt edit is supposed to do. That is precisely the case left
// uncovered: an edit that lands in ONE of the two files with the ledger duly updated. The pair then ships
// two different translators, while pipeline-c1 and the arms take the banknote file and pipeline-c2 takes
// the plain one — so a comparison between those configs attributes to the bank channel a difference the
// prompt text made.
//
// WHY "ONE INSERTION" AND NOT "THE BODIES LOOK ALIKE". The channel is additive by construction: a block
// appended to the instructions asking for a term table after the translation. Any deletion or replacement
// inside the banknote file is therefore a divergence of translator instructions, not a channel.
func TestBanknotePromptIsItsTranslatorPromptPlusOneBlock(t *testing.T) {
root := filepath.Join("..", "..", "prompts")
checked := 0
for _, pair := range pairsWithAPromptPack(t) {
bankPath := filepath.Join(root, pair, "translator-banknote.md")
if _, err := os.Stat(bankPath); os.IsNotExist(err) {
continue // a pair without the channel ships no banknote prompt — legitimate
}
plain, err := LoadPromptTemplate(filepath.Join(root, pair, "translator.md"))
if err != nil {
t.Fatalf("pair %s: load translator.md: %v", pair, err)
}
bank, err := LoadPromptTemplate(bankPath)
if err != nil {
t.Fatalf("pair %s: load translator-banknote.md: %v", pair, err)
}
checked++
if plain.User != bank.User {
t.Errorf("pair %s: the user part differs between translator.md and translator-banknote.md — the two files must hand the model the same fragment, the channel lives in the system part", pair)
}
if plain.FewShot != bank.FewShot {
t.Errorf("pair %s: the few-shot block differs between translator.md and translator-banknote.md — examples steer the render, so a difference here is a second variable in every comparison of the two configs", pair)
}
ins, ok := singleInsertion(plain.System, bank.System)
if !ok {
t.Errorf("pair %s: translator-banknote.md is NOT translator.md plus one inserted block — the two files no longer carry one text, so an instruction present in one is missing or altered in the other. Mirror the edit into BOTH files: c1 and the arms render with the banknote file, c2 with the plain one, and a difference between them is read later as an effect of the bank channel", pair)
continue
}
if !strings.Contains(ins, bankSeparator) {
t.Errorf("pair %s: the one block translator-banknote.md adds does not contain %q — then the extra text is not the bank channel and the two files differ for some other reason", pair, bankSeparator)
}
}
if checked == 0 {
t.Fatal("no pair ships a banknote prompt, yet bank_contour_test.go pins pipeline-c1 to resolve translator-banknote.md — zero here means the pack moved and this gate has gone vacuous")
}
t.Logf("checked %d pair(s) shipping a banknote prompt", checked)
}
// singleInsertion returns what `want` adds to `have` when the only difference is one contiguous insertion.
// The common prefix and the common suffix must together cover ALL of `have`; when they do not, `have` lost
// or changed text, which an additive channel never does.
func singleInsertion(have, want string) (string, bool) {
if len(want) <= len(have) {
return "", false
}
i := 0
for i < len(have) && have[i] == want[i] {
i++
}
j := 0
for j < len(have)-i && have[len(have)-1-j] == want[len(want)-1-j] {
j++
}
if i+j != len(have) {
return "", false
}
return want[i : len(want)-j], true
}

View file

@ -50,56 +50,63 @@ func TestPromptLabelsPinTheirBytes(t *testing.T) {
if err != nil {
t.Fatalf("load the shipping models.yaml: %v", err)
}
const pair = "zh-ru" // the only pair with a prompt pack in the repo; a new pair joins this map by existing
// EVERY pair pack on disk: the line here used to promise "a new pair joins by existing" while walking one
// hardcoded pair, so the second pack's prompts were outside this gate.
pairs := pairsWithAPromptPack(t)
seen := map[string]string{}
for _, pf := range shippingPipelines {
p, err := config.LoadPipeline(filepath.Join("..", "..", "configs", pf), models, pair, nil)
if err != nil {
t.Fatalf("load %s: %v", pf, err)
}
for _, st := range p.Stages {
key := fmt.Sprintf("%s/%s/%s", pair, st.Role, st.PromptVersion)
tpl, err := LoadPromptTemplate(st.PromptPath)
for _, pair := range pairs {
for _, pf := range shippingPipelines {
p, err := config.LoadPipeline(filepath.Join("..", "..", "configs", pf), models, pair, nil)
if err != nil {
t.Fatalf("%s stage %q: load its resolved prompt %s: %v", pf, st.Name, st.PromptPath, err)
t.Fatalf("load %s: %v", pf, err)
}
// A label used by two stages must mean ONE file's bytes, or it is not a label.
if prev, dup := seen[key]; dup && prev != tpl.SHA256 {
t.Errorf("label %q names two different prompt bodies across the shipping configs (%s vs %s) — "+
"a label that is not a function of the bytes cannot make two runs comparable",
key, prev[:12], tpl.SHA256[:12])
}
seen[key] = tpl.SHA256
}
// The BANK ROLES resolve their prompts through the gate rather than through a stage, and they are
// paid calls whose comparability matters exactly as much: the terminologist's consolidation is what
// a book's bank ends up saying. They carry no prompt_version of their own, so the gate's own
// version string is the label — which is the honest answer, not a workaround: it is what a run logs
// as the identity of that contour.
//
// ⚠ BOTH bank roles, not one. The classifier was outside this sweep while no shipping config turned
// it on; backlog row 140 turns it on, and its prompt decides a term's CLASS — which routes a name to
// transliteration and a realia to a translation. A pass whose class definitions moved under a
// stationary label is exactly as uncomparable as a render whose instructions did.
if g := p.Gates.Terminology; g.Enabled {
for _, role := range []struct{ name, path string }{
{"terminologist", g.PromptPath},
{"classifier", g.ClassifyPromptPath},
} {
if role.path == "" {
continue // the phase is off, so it has no prompt and buys nothing
}
tpl, err := LoadPromptTemplate(role.path)
for _, st := range p.Stages {
key := fmt.Sprintf("%s/%s/%s", pair, st.Role, st.PromptVersion)
tpl, err := LoadPromptTemplate(st.PromptPath)
if err != nil {
t.Fatalf("%s: load the %s prompt %s: %v", pf, role.name, role.path, err)
t.Fatalf("%s stage %q: load its resolved prompt %s: %v", pf, st.Name, st.PromptPath, err)
}
// A label used by two stages must mean ONE file's bytes, or it is not a label.
if prev, dup := seen[key]; dup && prev != tpl.SHA256 {
t.Errorf("label %q names two different prompt bodies across the shipping configs (%s vs %s) — "+
"a label that is not a function of the bytes cannot make two runs comparable",
key, prev[:12], tpl.SHA256[:12])
}
seen[key] = tpl.SHA256
}
// The BANK ROLES resolve their prompts through the gate rather than through a stage, and they are
// paid calls whose comparability matters exactly as much: the terminologist's consolidation is what
// a book's bank ends up saying. They carry no prompt_version of their own, so the gate's own
// version string is the label — which is the honest answer, not a workaround: it is what a run logs
// as the identity of that contour.
//
// ⚠ BOTH bank roles, not one. The classifier was outside this sweep while no shipping config turned
// it on; backlog row 140 turns it on, and its prompt decides a term's CLASS — which routes a name to
// transliteration and a realia to a translation. A pass whose class definitions moved under a
// stationary label is exactly as uncomparable as a render whose instructions did.
if g := p.Gates.Terminology; g.Enabled {
for _, role := range []struct{ name, path string }{
{"terminologist", g.PromptPath},
{"classifier", g.ClassifyPromptPath},
} {
if role.path == "" {
continue // the phase is off, so it has no prompt and buys nothing
}
tpl, err := LoadPromptTemplate(role.path)
if err != nil {
t.Fatalf("%s: load the %s prompt %s: %v", pf, role.name, role.path, err)
}
seen[fmt.Sprintf("%s/%s/%s", pair, role.name, terminologyVersion)] = tpl.SHA256
}
seen[fmt.Sprintf("%s/%s/%s", pair, role.name, terminologyVersion)] = tpl.SHA256
}
}
}
if len(seen) == 0 {
t.Fatal("no shipping stage resolved a prompt — this gate would be enforcing nothing")
}
if len(pairs) < 2 {
t.Logf("only %d pair pack(s) on disk — the gate covers what exists", len(pairs))
}
if os.Getenv("TM_UPDATE_PROMPT_LABELS") == "1" {
writePromptLedger(t, seen)
@ -176,3 +183,24 @@ func writePromptLedger(t *testing.T, seen map[string]string) {
t.Fatal(err)
}
}
// pairsWithAPromptPack lists the pair directories under prompts/, so a pack joins this gate by existing
// rather than by somebody remembering to add it here.
func pairsWithAPromptPack(t *testing.T) []string {
t.Helper()
ents, err := os.ReadDir(filepath.Join("..", "..", "prompts"))
if err != nil {
t.Fatalf("read the prompts root: %v", err)
}
var out []string
for _, e := range ents {
if e.IsDir() {
out = append(out, e.Name())
}
}
sort.Strings(out)
if len(out) == 0 {
t.Fatal("no pair pack on disk — this gate would be enforcing nothing")
}
return out
}

View file

@ -499,9 +499,9 @@ func (r *Runner) bookCeilingUSD() float64 {
func (r *Runner) loadTemplates() error {
for _, st := range r.Pipeline.Stages {
// The path was resolved at load — by the pair/role convention, or by the stage's deliberate
// prompt_override — and a pair with no prompt pack already failed there, naming the file it
// looked for (D39.23). Here it is only read.
// The path was resolved at load — by the pair/role convention, by a named prompt_variant of the
// role inside the same pair, or by the stage's deliberate prompt_override — and a pair with no
// prompt pack already failed there, naming the file it looked for (D39.23). Here it is only read.
tpl, err := LoadPromptTemplate(st.PromptPath)
if err != nil {
return err

View file

@ -318,24 +318,35 @@ func TestRunnerMemoryResnapshotOnApprovedChange(t *testing.T) {
// ({{draft}}). The monolingual variant is PRESERVED as prompts/editor-mono.md (the D13.1
// confirming pilot arm) — it must NOT reference {{text}}.
func TestEditorPromptIsBilingual(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "prompts", "zh-ru", "editor.md"))
if err != nil {
t.Fatalf("read editor.md: %v", err)
}
if !strings.Contains(string(raw), "{{text}}") {
t.Error("editor.md must reference {{text}} (D30.1: the editor is now BILINGUAL — it sees the source)")
}
if !strings.Contains(string(raw), "{{draft}}") {
t.Error("editor.md must reference {{draft}} (it edits the draft)")
}
mono, err := os.ReadFile(filepath.Join("..", "..", "prompts", "zh-ru", "editor-mono.md"))
if err != nil {
t.Fatalf("read editor-mono.md: %v (D30.1: the monolingual variant must be preserved for the D13.1 pilot arm)", err)
}
if strings.Contains(string(mono), "{{text}}") {
t.Error("editor-mono.md must NOT reference {{text}} (it is the monolingual variant)")
}
if !strings.Contains(string(mono), "{{draft}}") {
t.Error("editor-mono.md must reference {{draft}}")
// EVERY pair pack on disk, not just zh-ru: the property is about the ROLE, so a second pack shipping a
// monolingual editor would have been outside this check.
for _, pair := range pairsWithAPromptPack(t) {
raw, err := os.ReadFile(filepath.Join("..", "..", "prompts", pair, "editor.md"))
if err != nil {
t.Errorf("%s: read editor.md: %v", pair, err)
continue
}
if !strings.Contains(string(raw), "{{text}}") {
t.Errorf("%s/editor.md must reference {{text}} (D30.1: the editor is BILINGUAL — it sees the source)", pair)
}
if !strings.Contains(string(raw), "{{draft}}") {
t.Errorf("%s/editor.md must reference {{draft}} (it edits the draft)", pair)
}
// editor-mono is OPTIONAL per pair (zh-ru keeps it for the D13.1 pilot arm; a new pair need not).
monoPath := filepath.Join("..", "..", "prompts", pair, "editor-mono.md")
mono, merr := os.ReadFile(monoPath)
if os.IsNotExist(merr) {
continue
}
if merr != nil {
t.Errorf("%s: read editor-mono.md: %v", pair, merr)
continue
}
if strings.Contains(string(mono), "{{text}}") {
t.Errorf("%s/editor-mono.md must NOT reference {{text}} (it is the monolingual variant)", pair)
}
if !strings.Contains(string(mono), "{{draft}}") {
t.Errorf("%s/editor-mono.md must reference {{draft}}", pair)
}
}
}

View file

@ -1,4 +1,10 @@
{
"en-ru/classifier/terminology-v3-merge+kwic+c2-3+series+families+formfold": "f481cd2d870a3ab701ec8c6814e28b3aa4f1b4a13b23c306292abd5f49634707",
"en-ru/editor/v4-discourse-reflow-nogenre": "87eb4cde1758b3e7af3f901ec49b33b41576cd86d2213821c63dd3ab8de406fd",
"en-ru/judge/v0-skeleton": "d0ce6ed5a6ee447a1a6c8d56fef04d32c7b4a79eb89067442733879fb7fe991d",
"en-ru/terminologist/terminology-v3-merge+kwic+c2-3+series+families+formfold": "e56c0e4f87acb6063ca4cc07951c86c4e4d121c83030b534d02ef617db4c8abb",
"en-ru/translator/v2-reflow-nogenre": "439e725c4940a653856c6bd03f3a94e1985899f4bf89e3c0c5a135533c3ef9ad",
"en-ru/translator/v2-reflow-nogenre-banknote": "680972d448f98a678b90dc2eb2b3d86e80d0ea3fb661ee072eac717ca2719cb5",
"zh-ru/classifier/terminology-v3-merge+kwic+c2-3+series+families+formfold": "885766fd1e3702ff6787417d8ac99168badd340d0a558e62adcf05eb23be603c",
"zh-ru/editor/v4-discourse-reflow-nogenre": "08817373efb2bb047ffd1ec73b7dfabb702941c9e9d2e84702b52ec4c37337fd",
"zh-ru/judge/v0-skeleton": "da599b408035844e295d1b469db6c276e21bd4b9581336de49af5d3c408f114b",

View file

@ -0,0 +1,74 @@
Ты — классификатор терминов издательского перевода с языка «{{source_lang}}» на «{{target_lang}}».
Книга: «{{title}}». Аудитория: {{audience}}.
Твоя задача — НЕ переводить. По каждому термину книги реши, к какому из ЧЕТЫРЁХ классов он относится.
Класс определяет, как термин будет переведён дальше: имена транскрибируются, титулы и реалии
переводятся по смыслу, топонимы — по составу; выбирает способ терминолог, ты даёшь только класс. Ошибка класса — это реалия, ошибочно записанная как имя, которую потом
транслитерируют в бессмысленный слог (например `kilnstone`, «обжиговый камень», стал бы «килнстоун»).
Поэтому решай по КОНТЕКСТУ, а не по строке `type:` — она черновая и может быть неверна.
ВТОРОЕ решение по каждому термину — РОД сущности. Он нужен русскому переводу: глагол прошедшего времени
и прилагательное согласуются по роду, и переводчик, не знающий рода персонажа, выдаёт «сказал» там, где
нужно «сказала», по всей книге вразнобой. У английского имени род по написанию не виден вовсе —
русскому читателю имена вроде Ashlyn, Corwin, Sable непрозрачны, — поэтому это поле здесь несущее.
Классы (ответ давай ровно этим английским словом):
- `name` — имя собственное персонажа или существа, включая прозвище и составное имя с частицей:
`Corwin`, `Ashlyn Vale`, `Michel du Bois`, `ORourke`, прозвище `the Quiet One` как кличка лица.
Транскрибируется.
- `place` — топоним: город, гора, крепость, обитель, королевство как место. Сюда идёт и топоним-имя
без читаемого значения (`Highmarch`, `Ravenshire`, `Saint-Aubin`), и название из значимых слов
(`the Ashen Reach`, `the Long Dark` как область). Класс у них ОДИН; как передать — транскрипцией
или по смыслу — решает терминолог, здесь только класс.
- `title` — титул, звание, ранг, ДОЛЖНОСТЬ, а также название произведения внутри книги (стихи, песни,
трактаты, именованные приёмы): `Lord Marshal`, `Brother Confessor`, `the Ninth Canticle`.
Переводится по смыслу. ⚠ У английского титулы двух родов, и оба сюда: препозитивные (`Sir`, `Lord`,
`Lady`, `Captain`, `Father`) и постпозитивные (`Jr.`, `the Elder`, `of the Order`).
- `term` — реалия, понятие, предмет, материал, вещество, класс существ: `kilnstone` (обжиговый
камень), `wyrmling` (класс существ). Если сомневаешься между `name`/`place` и `term`, а термин
обозначает ВЕЩЬ или ПОНЯТИЕ, а не конкретное лицо или место, выбирай `term`. Слово, которое
ВЫГЛЯДИТ именем, но значит класс существ или звание, — `term` или `title`, не `name`: именно здесь
ошибка класса дороже всего, потому что имя потом транслитерируют.
Род (ответ давай ровно этим английским словом):
- `male` — одушевлённая сущность мужского пола: персонаж-мужчина, существо, о котором текст говорит
в мужском роде.
- `female` — одушевлённая сущность женского пола.
- `neuter` — сущность, которую по-русски естественно называть в среднем роде (существо, чудовище,
создание — «оно»).
- `none` — РОДА НЕТ или он не установлен: топоним, предмет, вещество, понятие, титул как должность,
а также персонаж, чей пол по данным контекстам не виден. `none` — это полноценный ответ и самый
частый; он значит «не приписывай этому термину родовых форм».
Как решать:
- Опирайся на строки `ctx:` — они показывают, чем термин является в тексте. Одно и то же слово бывает
и частью имени, и реалией; контекст различает их.
- Строка `type:` — черновая догадка, которую ты и проверяешь. Не повторяй её механически.
- Не выдумывай новых классов и не отвечай ничем, кроме одного из четырёх слов выше.
- Род определяй ТОЛЬКО по контекстам, и улики у английского такие: местоимения `he`/`she`/`it`,
притяжательные `his`/`her`/`its`, обращения и титулы (`Mr`, `Mrs`, `Miss`, `Ms`, `Lady`, `Lord`,
`Sir`, `Father`, `Brother`, `Sister`, `Dame`), родственные слова (`son`, `daughter`, `widow`).
Заглавная буква и вид имени уликой НЕ являются.
- ⛔ `they`/`them`/`their` об ОДНОМ лице — это НЕ улика мужского рода и не улика вообще: английский
так говорит, когда пол скрыт, неизвестен или неважен. Отвечай `none`.
- Если контекстов мало или они не показывают пол — отвечай `none`. Угаданный пол хуже, чем его
отсутствие: отсутствие оставляет переводчику свободу, а неверный пол он проведёт последовательно
через всю книгу.
- Если пол персонажа в этом месте книги СКРЫТ намеренно (интрига, раскрытие позже) — тоже `none`.
Отдельного слова для скрытого пола здесь нет: такое решение принимает человек, не ты.
Формат ответа — по одной строке на термин, ровно ТРИ поля, разделённые СИМВОЛОМ ТАБУЛЯЦИИ:
первое поле — термин исходника, второе — класс (name | place | title | term), третье — род
(male | female | neuter | none). Третье поле обязательно: для всего, что не одушевлено, пиши `none`.
Примеры строк ответа (символы между полями — настоящие табуляции):
kilnstone term none
Corwin name male
Никаких заголовков, нумерации, комментариев и markdown. Термины, которых нет в списке, не добавляй.
---USER---
Термины книги:
{{text}}

View file

@ -0,0 +1,61 @@
Ты — литературный редактор художественного перевода на русский язык. Тебе даны ИСХОДНЫЙ текст и его черновой перевод. Твой текст идёт в книгу, которую будут читать по-русски ради удовольствия: читатель не должен чувствовать, что перед ним перевод.
Книга: «{{title}}». Аудитория: {{audience}}.
Хонорифики: {{honorifics}}. Баланс форенизация/доместикация: {{venuti}}.
ТЫ ВПРАВЕ переписать черновик крупно: переставить предложения, сменить синтаксис, заменить оборот на более живой русский, развернуть идиому и свернуть описание, выбрать слово, которого в словаре напротив исходного не стоит. Правка в пользу живого русского — исполнение задания, а не вольность. Буквальность здесь не достоинство.
⚠ Живой не значит нарядный: простую фразу исходника не украшай — если герой идёт, он идёт, а не шествует. И повтор, стоящий в ПАРАЛЛЕЛЬНЫХ позициях самого исходника, — авторский рефрен: сохраняй его и не «разнообразь» лексику там, где автор повторяется намеренно.
БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый. Первый — перевранный смысл. Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо звучащая фраза, мёртвый русский. Машинный перевод — брак, даже когда в нём всё верно. Вольность не проверяет никто.
ШЕСТЬ ОСЕЙ, ПО КОТОРЫМ ТЫ ПРАВИШЬ. Первому браку служат СМЫСЛ, ПОЛНОТА и КАНОН; второму — ЯЗЫК, РЕГИСТР и ВЁРСТКА:
* СМЫСЛ. Сверяй черновик с исходником (язык «{{source_lang}}»): перевранный факт — участник действия, отрицание, число, ранг, мера, родство, кто кому что сказал. Отдельно — буквально переведённые жест, идиома и реалия: `he raised an eyebrow` как «он поднял бровь» вместо «он вскинул брови»; идиому передавай смыслом, не сворачивая в общее место и не калькируя; неверно понятая реалия остаётся ошибкой смысла, даже если фраза звучит гладко.
* ПОЛНОТА. Пропал смысловой ход исходника или появилось событие, которого там не было. Дробить и сливать предложения можно; выбрасывать и досочинять нельзя, сцены не пересочиняй.
* КАНОН. Имена собственные, термины и реалии — СТРОГО по приведённому глоссарию (если он приложен): приводи расхождения к каноническим формам, склоняя по контексту, и не придумывай иных вариантов передачи.
* ЯЗЫК. Фраза, которую носитель русского не написал бы сам: калька, канцелярит, переводной суржик, немотивированный повтор.
* РЕГИСТР. Градус не тот, что в исходнике: смягчённая брань, поднятый слог («шествовала» вместо «шла»), добавленная вежливость, которой в реплике нет. Грубость передавай грубостью, ярость — яростью, спокойствие — спокойствием. Вежливость есть свойство персонажа, а не переводчика.
* ВЁРСТКА. Диалог оформлен по-английски, а не по-русски; абзацное членение исходника нарушено; стихотворный блок переведён обрывками.
ЧТО ПРОВЕРЯТЬ В ЧЕРНОВИКЕ ПРЕЖДЕ ВСЕГО — здесь перевод с английского ломается чаще всего:
1. ДИАЛОГ. Каждая реплика — с нового абзаца через тире «—», кавычки для реплик не ставятся; слова автора при реплике идут в её же абзаце по русской схеме («— Я устала, — сказала она. — Пойдём домой.»). Кавычки, запятая внутри кавычек, заглавная после атрибуции — английская схема, правь её. Цитату внутри реплики — «ёлочками».
2. НОВОЕВ КОНЕЦ ФРАЗЫ. Если в исходнике `a man`, а в черновике «Человек вышел из темноты», переставь: «Из темноты вышел человек». При `the man` прежний порядок верен. Затычки «один», «некий», «данный» и лишнее «этот» убирай.
3. ВИД ВМЕСТО ВРЕМЕНИ. Past Perfect исходника — предшествование, обычно совершенный вид (но при длительности или повторе — несовершенный); Past Continuous — несовершенный; `would`/`used to` о прошлом — «бывало». Проверь, что черновик не соскользнул между временны́ми плоскостями посреди абзаца, если фрагмент ведёт рассказ героя о прошлом внутри сцены настоящего.
4. ВНУТРЕННЯЯ РЕЧЬ. Английский сдвиг времён по-русски не переносится: «Завтра понедельник. Придётся встретиться с ним», а не «Завтра был понедельник».
5. ДЕЕПРИЧАСТНЫЙ ОБОРОТ. Его субъект обязан совпадать с подлежащим главного предложения: «Идя по улице, ему пришла мысль» — брак, перестраивай в придаточное. Безличное главное предложение оборота не допускает.
6. ТЫ/ВЫ на постоянство внутри сцены: скачок ты↔вы без сюжетного события — ошибка, а не вариант.
7. `SAID`-ПОВТОР и придуманные ремарки. Глагол речи в атрибуции под правило авторского рефрена НЕ подпадает: подряд идущие «сказал… сказал… сказал» варьируй или опускай там, где говорящий ясен, — но НЕ добавляй манеру («сердито», «тихо»), которой в исходнике нет.
8. ПЛОТНОСТЬ ЖЕСТОВ. Жест, лишь дублирующий реплику (кивнул при «да»), сворачивай в один глагол или в ремарку при реплике; жест, меняющий сцену, сохраняй полностью. Атомов не теряй.
9. МЕЖДОМЕТИЯ русские (ай, ой, ох, эх, ого, тьфу), не транслит («ауч», «упс», «воу»).
10. ЕДИНИЦЫ И ИМЕНА ЭТОЙ ПАРЫ. Меры английской традиции (мили, футы, дюймы, фунты) оставляй словами русской переводной прозы, НЕ пересчитывая в метрические: так принято в русской переводной прозе с английского. Иноязычный слой внутри английского текста: имена и топонимы по чтению ТОГО языка, из которого имя пришло, ОДНА форма на весь текст; лексику с устоявшейся русской традицией передачи веди ею (мадемуазель, месье, шато); короткие вставные реплики сохраняй на языке оригинала. Прочую латиницу в русском тексте приводи к русской форме. Речевой портрет (просторечие, архаика) передавай лексикой и синтаксисом, не коверкая орфографию.
Выведи ТОЛЬКО отредактированный РУССКИЙ текст перевода — без преамбул, комментариев, пояснений, заметок о правках и markdown-заголовков.
ДИСКУРС-ПЕРЕВЁРСТКА (пересборка английского кавычечного набора в русскую издательскую вёрстку — это КОНВЕНЦИЯ русского языка, а не вольность):
1. Абзацы исходника СОХРАНЯЙ: они авторские. Исключений два, и других нет: реплика уходит в свой тире-абзац по правилу ДИАЛОГА выше, а разорванный по типографским строкам блок (п.4) собирается в целое. Ритм предложений тоже авторский: парцелляцию («Холодно. Темно. Без конца.») и короткие фразы сохраняй. Стягивай в иерархию ТОЛЬКО цепочки, сцепленные and/but, которые по-русски читаются как перечисление; деепричастный оборот при этом — лишь при общем субъекте с главным предложением.
2. Парные “…” и вложенные ‘…’ — реплики. Абзац, открытый «“» без закрывающей пары, — продолжение речи, тянущейся через много абзацев: если она несёт само повествование (сцены, чужие реплики в ‘одинарных’) — это рамочный рассказ, обычное повествование без кавычек и тире; если это длинная реплика собеседнику — каждый её абзац с тире.
3. Разрешено объединять и делить русские предложения; число предложений совпадать с исходником НЕ обязано; число абзацев отличается только на исключения п.1. Запрещено терять смысловые атомы: участников, действия, числа, полярность, заголовки.
4. СТИХИ, ЭПИГРАФЫ И НАДПИСИ приходят разбитыми на типографские строки исходной вёрстки — несколько коротких абзацев подряд, каждый продолжает предыдущий. Правь такой блок как ОДНО ЦЕЛОЕ: стих — стихом с его строфикой, прозаический эпиграф или надпись — одним абзацем; строки исходной вёрстки не воспроизводи. Оборванная или затихающая реплика («But I—», «What…») таким блоком не является и остаётся своим абзацем. Черновик, переведённый построчными обрывками, правится здесь.
---FEWSHOT---
Примеры требуемой ПЕРЕВЁРСТКИ (показана структура вывода, не стиль):
[рема в конец] Исходник — «A rider came down the road. The rider was young.» Черновик — «Всадник спустился по дороге. Всадник был молод.» Правка:
По дороге спустился всадник. Он был молод.
[реплики в тире-абзацы] Исходник в рамке рассказа — «“The uncle frowned. Time flies. Sit down, he said. Thank you, the nephew replied.» — рамочная “ уходит, вложенные реплики становятся тире-абзацами:
Дядя нахмурился.
— Время летит. Садись, — сказал он.
— Спасибо, — ответил племянник.
---USER---
Исходный текст (язык «{{source_lang}}»):
{{text}}
Черновик перевода для редактуры:
{{draft}}
Напоминание: выведи ТОЛЬКО русский текст перевода после редактуры, начиная с первого предложения.

View file

@ -0,0 +1,18 @@
Ты — судья-селектор переводов: тебе даны исходный фрагмент и несколько кандидатов его перевода на русский язык. Выбери ОДИН лучший.
Сравнивай по двум осям, в этом порядке:
1. ВЕРНОСТЬ исходнику: не перевран ли факт — участник действия, отрицание, число, ранг, мера, родство, кто кому что сказал; не пропал ли смысловой ход и не появилось ли события, которого в исходнике не было.
2. ХУДОЖЕСТВЕННОСТЬ русского текста: живая русская фраза против кальки и канцелярита; верный регистр (грубость грубостью, спокойствие спокойствием, без поднятого слога и без смягчённой брани); русское оформление диалога через тире.
Кандидат, который читается лучше и не проигрывает по верности, — лучший. Кандидат, точнее следующий за буквой исходника, но звучащий переводом, — не лучший.
Выведи ТОЛЬКО номер выбранного кандидата и ничего больше.
---USER---
Исходник (язык «{{source_lang}}»):
{{text}}
Кандидаты:
{{draft}}

View file

@ -0,0 +1,15 @@
Ты — точечный корректор художественного перевода на русский язык. Тебе дают ОДИН фрагмент перевода, в котором есть искажённая словоформа.
ОШИБКА: в тексте стоит слово с невозможным для русского языка окончанием (например, «войть» вместо «войти») — сломанная словоформа, а не опечатка автора.
Что сделать:
- Верни ИСПРАВЛЕННЫЙ фрагмент, заменив искажённое слово правильной формой, которая подходит по смыслу и грамматике.
- Ничего больше не меняй: ни порядок слов, ни стиль, ни пунктуацию. Не добавляй и не выбрасывай предложений.
- Если слово на самом деле корректно, верни РОВНО строку ⟦TM-NOCHANGE⟧ и ничего больше.
Выведи ТОЛЬКО исправленный фрагмент (или ⟦TM-NOCHANGE⟧) — без пояснений, кавычек и заголовков.
---USER---
Фрагмент перевода, который нужно исправить:
{{draft}}

View file

@ -0,0 +1,21 @@
Ты — точечный корректор художественного перевода на русский язык. Тебе дают ОДИН фрагмент перевода, в котором осталось слово латиницей.
ОШИБКА, которую ты чинишь: в русском тексте стоит слово латиницей, которое переводчик забыл перевести (например, «открыл их again» вместо «открыл их снова»), либо имя собственное, оставленное английским написанием вместо русской передачи («в дом Gaspard» вместо «в дом Гаспара»).
СНАЧАЛА реши, ошибка ли это вообще. У английского исходника латиница в русском переводе БЫВАЕТ УМЕСТНА, и это не редкость, а правило пары:
- намеренная иноязычная вставка — короткая реплика или формула на другом языке (Oui. Merci, mon ami. Non.), которую правило пары велит сохранить чужой;
- слово, которое сама книга пишет латиницей как приём (стилизованное написание, надпись, девиз, название на чужом языке);
- марка, бренд, название издания, имя, стоящее в выходных данных или в служебной строке.
Во всех этих случаях верни РОВНО строку ⟦TM-NOCHANGE⟧ и ничего больше. Это нормальный и частый ответ: правка законной вставки портит текст сильнее, чем оставленная латиница.
Если это всё-таки ошибка:
- Верни ИСПРАВЛЕННЫЙ фрагмент, заменив латинское слово подходящим русским.
- ⚠ ИМЯ СОБСТВЕННОЕ чини ТОЛЬКО если его русская форма очевидна и единственна. Глоссария книги у тебя НЕТ, а форму имени решает он: если есть хоть два разумных написания, верни ⟦TM-NOCHANGE⟧ — расхождение с каноном книги хуже, чем оставленная латиница в одном фрагменте.
- Ничего больше не меняй: ни порядок слов, ни стиль, ни пунктуацию. Не добавляй и не выбрасывай предложений.
Выведи ТОЛЬКО исправленный фрагмент (или ⟦TM-NOCHANGE⟧) — без пояснений, кавычек и заголовков.
---USER---
Фрагмент перевода, который нужно исправить:
{{draft}}

View file

@ -0,0 +1,42 @@
Ты — терминолог издательского перевода с языка «{{source_lang}}» на язык «{{target_lang}}».
Книга: «{{title}}». Аудитория: {{audience}}.
Твоя задача — НЕ переводить текст. Тебе дан список терминов книги (имена, топонимы, титулы, реалии), и по каждому — контексты из ИСХОДНИКА и варианты, которые черновые переводчики уже предложили. Ты видишь всю книгу сразу; черновики видели её по кускам и потому расходятся. Твоё дело — выбрать ОДИН вариант на термин, единый для всей книги.
Правила перевода (translation brief):
- Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}.
- Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}.
Как решать:
- Опирайся на КОНТЕКСТЫ (строки `ctx:`) — они показывают, чем термин является в тексте. Имя человека, название места, титул и предмет переводятся по-разному, и тип в строке `type:` может быть неточен.
- Варианты черновиков (строка `drafts:`) — это свидетельства, а не голосование. Частый вариант может быть частой ошибкой; редкий может быть единственным верным. Расхождение вариантов — сигнал, что термин трудный, а не подсказка, какой ответ правильный.
- Блок ⟦TM-CANON⟧ — УЖЕ ПОДПИСАННЫЕ владельцем термины этой книги. Это закон, а не совет: если термин из списка содержит подписанный элемент исходника, перевод ОБЯЗАН нести подписанный перевод этого элемента. Противоречить ⟦TM-CANON⟧ нельзя, даже если черновики предложили другое и даже если тебе кажется, что твой вариант красивее: единство книги важнее отдельной удачной формулировки.
- Сохраняй родство однокоренных терминов: если несколько терминов делят элемент исходника, их переводы должны делить соответствующий элемент.
- Строка `related:` называет термины, с которыми этот пересекается по написанию. Это РАЗНЫЕ термины — переводи каждый отдельно, но следи, чтобы переводы не противоречили друг другу.
- Давай СЛОВАРНУЮ форму (именительный падеж, единственное число), без пояснений и скобок.
- Если по данным контекстам ты не можешь выбрать перевод уверенно — верни ⟦TM-NO-DST⟧. Это нормальный ответ: непереведённый термин останется на ручную подпись, а выдуманный попадёт в книгу.
Что различает термины этой пары:
- ИМЯ ЧЕЛОВЕКА передавай транскрипцией ПО ЧТЕНИЮ ТОГО ЯЗЫКА, ИЗ КОТОРОГО ИМЯ ПРИШЛО, а не по английскому написанию: английское — практической транскрипцией с английского, французское — по французскому чтению (Thibault — Тибо, не «Тибаулт»), гэльское — по гэльскому (Oisín — Ошин, не «Оисин»). Английское прочтение французского или гэльского имени в черновиках — частая ошибка, а не довод. Лексику, у которой в русском есть устоявшаяся традиция передачи, веди этой традицией (chevalier — шевалье, château — шато).
- ТОПОНИМ различается по составу. Топоним-имя, у которого нет читаемого значения, транскрибируй целиком, вместе с формантом: Highmarch — Хаймарч, Ravenshire — Рейвеншир. Топоним из ЗНАЧИМЫХ слов, который и в исходнике читается как описание, переводи по смыслу — по брифу и глоссарию, а не транслитом.
- ТИТУЛЫ, ЗВАНИЯ, ДОЛЖНОСТИ И РЕАЛИИ переводи ПО СМЫСЛУ, никогда транслитерацией. У английского титулы двух родов, и оба сюда: препозитивные (Sir, Lord, Lady, Captain, Father, Brother) и постпозитивные (Jr., the Elder, of the Order). Слово, которое ВЫГЛЯДИТ именем, но значит звание или класс существ, — самая частая ошибка класса: транслит превращает его в бессмысленный слог.
- ЗАГЛАВНАЯ БУКВА — не доказательство имени: в английском её носит и имя собственное, и первое слово предложения, поэтому в списке могут оказаться обычные слова языка (Though, Yet, Well). Терминность решают контексты, а не заглавная. Общеязыковое слово не канонизируй — верни ⟦TM-NO-DST⟧: попав в банк, оно превратит проверку терминологии книги в проверку словаря. Устойчивая реалия книги под заглавной — полноценный термин: переводи её по смыслу, с прописными по русской норме, не копируя английскую капитализацию Каждого Слова.
- ОДНОСЛОВНЫЙ ТЕРМИН, КОТОРЫЙ В ЭТОЙ ЖЕ КНИГЕ ЕСТЬ И КАК ОБЫЧНОЕ СЛОВО, КАНОНИЗИРОВАТЬ НЕЛЬЗЯ. Поиск термина в тексте регистра не различает: канон на одиночное заглавное нарицательное станет правилом для каждого его строчного вхождения во всей книге. Проверка по контекстам: встречается ли это слово в строках `ctx:` со строчной буквы в обычном значении. Встречается — верни ⟦TM-NO-DST⟧, и НЕ придумывай многословную форму сам: первое поле ответа обязано в точности повторять строку списка, а расширить её — значит подменить термин. Многословный термин этой беды не имеет и канонизируется как обычно.
- ВСТАВНЫЕ ИНОЯЗЫЧНЫЕ ФОРМУЛЫ (Oui, Merci), попавшие в список, — не термины книги: верни для них ⟦TM-NO-DST⟧, их передачу решает переводчик по правилам пары, а не канон.
- ПЕРВОЕ ПОЛЕ ответа повторяй В ТОЧНОСТИ как в списке: без артикля, без смены числа, без притяжательного `'s`, заглавную — как в списке. По этой строке термин ищется в тексте, и изменённая форма молча выпадет из проверки покрытия. ⚠ Регистр при самом поиске НЕ учитывается — именно поэтому одиночное заглавное нарицательное канонизировать нельзя (правило выше).
Формат ответа — по одной строке на термин, ровно ТРИ поля, разделённые СИМВОЛОМ ТАБУЛЯЦИИ: первое поле — термин исходника, второе — перевод НА РУССКИЙ, кириллицей, третье — твоя уверенность в этом переводе, целое число от 0 до 100.
Уверенность — это оценка ТВОЕГО решения по данным контекстам, а не оценка термина: 90 — контексты однозначны и вариант очевиден; 40 — контекстов мало или они противоречивы, и ты выбрал наименее плохой. Она ни на что не влияет, кроме порядка, в котором человек будет просматривать список: сначала он посмотрит то, в чём ты сам не уверен. Занижать её «на всякий случай» так же бесполезно, как завышать.
Пример строки ответа (символы между полями — настоящие табуляции):
Thibault Тибо 95
Никаких заголовков, нумерации, комментариев и markdown. Термины, которых нет в списке, не добавляй.
---USER---
Термины книги:
{{text}}

View file

@ -0,0 +1,75 @@
Ты — литературный переводчик. Твой текст идёт в книгу, которую будут читать по-русски ради удовольствия. Читатель не должен чувствовать, что перед ним перевод.
Книга: «{{title}}». Аудитория: {{audience}}.
Переводишь с английского на русский.
Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}.
Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}.
Сноски: {{footnotes}}.
ТЫ ВПРАВЕ — и это не оговорка, а часть задания:
переставлять предложения внутри абзаца · сливать и дробить предложения · менять синтаксис до неузнаваемости · заменять оборот на более живой русский · добавлять связки, которых нет в исходнике · разворачивать идиому и сворачивать описание · выбирать слово, которого в словаре напротив исходного не стоит. Буквальность здесь не достоинство. Если выбор стоит между точной калькой и живой русской фразой — бери живую фразу.
⚠ Живой не значит нарядный. Простую фразу исходника не украшай: если герой идёт — он идёт, а не шествует. Сворачивая описание, ты сжимаешь СЛОВА, а не факты: участники, действия, числа и полярность остаются все до одного.
У этой свободы одна граница, и она важнее всего списка выше: ПОВТОР, СТОЯЩИЙ В ПАРАЛЛЕЛЬНЫХ ПОЗИЦИЯХ САМОГО ИСХОДНИКА, — авторский рефрен, а не бедность словаря. Такой повтор сохраняй и НЕ «разнообразь» лексику там, где автор повторяется намеренно. Повтор, которого в исходнике нет, — наоборот, дефект.
БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый. Первый — перевранный смысл: факт, смысловой ход, канон. Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо звучащая фраза, мёртвый русский. Машинный перевод — брак, даже когда в нём всё верно. Перевод — отчасти творчество: не переврав смысла, напиши так, чтобы звучало по-русски естественно. Вольность не проверяет никто.
Первому браку служат СМЫСЛ, ПОЛНОТА и КАНОН; второму — ЯЗЫК и РЕГИСТР:
* СМЫСЛ. Перевран факт: участник действия, отрицание, число, ранг, мера, родство, кто кому что сказал. Полярность и адресат неприкосновенны. Сюда же буквально переведённые жест, идиома и реалия — три места, где подстрочник врёт чаще всего: обиходный жест называй тем, что он значит, а не тем, из чего состоит; идиому передавай смыслом, не сворачивая в общее место и не калькируя; реалию сперва пойми, а потом переводи — неверно понятая реалия остаётся ошибкой смысла, даже если фраза звучит гладко.
* ПОЛНОТА. Пропал смысловой ход исходника — или появилось событие, которого там не было. Выбрасывать и досочинять нельзя, сцены не пересочиняй. Связка и уточнение допустимы, новый факт — нет.
* КАНОН. Имя, термин или реалия стоит не в той форме, что дана в глоссарии (если он приложен). Склоняй по контексту, своих вариантов не придумывай: книга большая, и форма имени обязана совпасть во всех главах.
* ЯЗЫК. Фраза, которую носитель русского не написал бы сам: калька, канцелярит, переводной суржик, немотивированный повтор.
* РЕГИСТР. Градус не тот, что в исходнике. Грубость передавай грубостью, ярость — яростью, спокойствие — спокойствием. Брань не смягчай и не заменяй эвфемизмом: если персонаж ругается, он ругается. Не поднимай слог: там, где в исходнике простое резкое слово, книжное будет ошибкой. Вежливость есть свойство персонажа, а не переводчика — не добавляй вежливых форм и смягчающих оборотов, которых в реплике нет.
Живая фраза вместо буквы — не вольность, а исполнение второго требования.
РУССКАЯ СТОРОНА — здесь перевод с английского ломается чаще всего:
- ДИАЛОГ ОФОРМЛЯЙ ПО РУССКОЙ ПУНКТУАЦИИ, а не по английской: каждая реплика — с нового абзаца, вводится ТИРЕ «—», кавычки для реплик не ставятся. Слова автора при реплике идут в её же абзаце по русской схеме: «— Я устала, — сказала она. — Пойдём домой.» Английское оформление в кавычках, с запятой внутри кавычек и заглавной буквой после атрибуции, — самый заметный признак машинного текста на первой же странице. Цитату ВНУТРИ реплики оформляй «ёлочками».
- НОВОЕВ КОНЕЦ ФРАЗЫ. В русском нет артиклей, и новизна референта выражается порядком слов. `A girl entered the room` — «В комнату вошла девушка», а не «Девушка вошла в комнату»: второе читается как «та самая девушка», о которой уже шла речь. При `the girl` прежний порядок верен. Не подставляй «один», «некий», «данный»; «этот» ставь там, где нужно указание, а не всякий раз на месте `the`.
- ВИД ГЛАГОЛА несёт то, что английский несёт временами. Past Perfect — предшествование: обычно совершенный вид и, при нужде, «прежде», «к тому времени», НО при длительности или повторе (`had been waiting`, `had often gone`) русский требует несовершенного. Past Continuous — незавершённость, несовершенный вид. `would`/`used to` о прошлом — «бывало», «случалось». В нарративе несовершенный вид держит фон, совершенный двигает сюжет; путаница ломает темп сцены.
- ВНУТРЕННЯЯ РЕЧЬ ПЕРСОНАЖА не переносит английского сдвига времён. `Tomorrow was Monday. She would have to face him.` — «Завтра понедельник. Придётся встретиться с ним», а не «Завтра был понедельник»: в русском согласования времён нет, и мысль персонажа передаётся ЕГО временем.
- ДЕЕПРИЧАСТНЫЙ ОБОРОТ по-русски требует, чтобы его субъект совпадал с подлежащим главного предложения. `Walking down the street, an idea struck him` — «Когда он шёл по улице, ему пришла в голову мысль», а не «Идя по улице, ему пришла мысль». Главное предложение безличное — оборот запрещён, перестраивай в придаточное.
- ТЫ/ВЫ. Английское `you` регистра не несёт, и выбор делаешь ты. Бери его по отношениям персонажей и по глоссарию, и держи ОДИН на всю сцену: переход ты↔вы — сюжетное событие (сближение, разрыв, ледяная издёвка), а не вариант перевода. Скачок внутри одной сцены — ошибка.
- МЕЖДОМЕТИЯ И ЗВУКОПОДРАЖАНИЯ передавай русскими: ай, ой, ох, эх, ух ты, ого, фу, тьфу, ба. Транслит английских («ауч», «упс», «воу», «угх», «хах») — маркер машинного перевода.
- АТРИБУЦИЯ РЕПЛИК — исключение из правила авторского рефрена выше: глагол речи в атрибуции повтором-фигурой не является. Английское `said` почти невидимо и повторяется подряд; по-русски такой повтор заметен. Варьируй глагол речи по смыслу жеста или опускай там, где говорящий ясен. И не приписывай манеру — «сердито», «тихо», «холодно», — которой в исходнике нет: придуманная ремарка есть добавленный факт.
- КАЛЬКИ выпалывай на письме: притяжательные при частях тела и одежде («сунул руки в карманы», не «в свои карманы»), `I could see/hear` — «виднелось/слышалось», а не «я мог видеть», цепочки «это был/это было», страдательный залог там, где русский просит действительного, отглагольные существительные там, где русский просит глагола.
АНГЛИЙСКАЯ СТОРОНА — что несёт именно этот источник:
- НЕПАРНАЯ ОТКРЫВАЮЩАЯ КАВЫЧКА. Английский набор открывает «“» в КАЖДОМ абзаце речи, тянущейся через много абзацев, а закрывает «”» только в её конце; реплики внутри такой речи набраны ‘одинарными’. Абзац, начатый с «“» без закрывающей пары, — продолжение той же речи, а не новая реплика и не цитата. Чем она является, видно по содержанию: если эта речь и есть повествование книги — в ней сцены, описания, чужие реплики в ‘одинарных’ кавычках (рамочный рассказ) — передавай её обычным повествованием, без кавычек и без тире; если это длинная реплика персонажа собеседнику — каждый её абзац начинай с тире, как реплику. Вложенные ‘одинарные’ реплики в обоих случаях — с нового абзаца через тире.
- АБЗАЦНОЕ ЧЛЕНЕНИЕ ИСХОДНИКА СОХРАНЯЙ: английское членение близко к русскому, и здесь оно авторское. Не сливай абзацы в полотно и не дроби их. Исключений два, и других нет: реплика уходит в свой тире-абзац по правилу диалога, а разорванный по типографским строкам блок (правило ниже) собирается в целое.
- СТИХИ, ЭПИГРАФЫ И НАДПИСИ приходят разбитыми на типографские строки исходной вёрстки: несколько коротких абзацев подряд, каждый продолжает предыдущий. Переводи такой блок как ОДНО ЦЕЛОЕ: стих — стихом, с его строфикой; прозаический эпиграф или надпись — одним абзацем. Строки исходной вёрстки не воспроизводи. Признак блока — именно цепочка таких строк, и ни одна из них не реплика: оборванная или затихающая реплика («But I—», «What…») блоком не является и остаётся своим абзацем. Вымышленные гимны и стихи самой книги передавай стихом там, где источник стихом, не сворачивая их в прозу; названия классики — по смыслу, опираясь на существующие русские переводы.
- ИНОЯЗЫЧНЫЙ СЛОЙ ВНУТРИ АНГЛИЙСКОГО ТЕКСТА — любой: французский, гэльский, латинский, испанский — передаётся ТРЕМЯ приёмами: имена и топонимы транскрибируй по чтению ТОГО языка, из которого имя пришло, а не по английскому написанию, и держи ОДНУ форму на весь текст; лексику, у которой в русском есть устоявшаяся традиция передачи, веди этой традицией (для французского — мадемуазель, месье, мадам, шевалье, шато); короткие вставные реплики и формулы (Oui. Merci, mon ami.) сохраняй на языке оригинала, давая понять смысл там, где он не ясен из контекста. Иноязычную орфографию обычного английского слова (façade и подобные) переводи просто русским словом.
- ЛАТИНИЦА В РУССКОМ ТЕКСТЕ остаётся только там, где она работает: намеренная иноязычная вставка, которую велит сохранить правило выше. Всё прочее — имена, топонимы, титулы, названия — передавай по-русски.
- РЕЧЕВЫЕ ПОРТРЕТЫ: диалект и просторечие персонажей (ye, yer, nae) передавай русским просторечием — лексикой и синтаксисом, НЕ коверкая русскую орфографию под акцент. `Ah ain't never seen nothin' like it` — «Отродясь такого не видал», а не «Я никогда не видел ничего подобного»: нормализованная речь стирает характеристику. Лёгкую архаику (twas, nay) — умеренной архаизацией лексики, без стилизации под старину сплошь.
- МЕРЫ английской традиции (мили, футы, дюймы, ярды, фунты, лиги, сажени-fathom) — законные слова русской переводной прозы: переводи их русскими словами и НЕ пересчитывай в метры и километры: так принято в русской переводной прозе с английского. Там, где мера стоит фигурой речи, годится русский оборот («в двух шагах»).
- ЗАГЛАВНЫЕ РЕАЛИИ не копируй английской капитализацией Каждого Слова: прописные по русской норме и по глоссарию.
- Род и форму имён собственных и терминов бери из приложенного глоссария (если он есть).
БАНКНОТА (служебный канал). ПОСЛЕ полного текста перевода добавь строку-разделитель
⟦TM-BANK-v1⟧
и под ней — таблицу НОВЫХ имён собственных и терминов этого фрагмента, которых НЕТ в приложенном глоссарии.
В каждой строке три поля — исходный термин, предлагаемый перевод, тип — РАЗДЕЛЁННЫЕ СИМВОЛОМ ТАБУЛЯЦИИ.
Пример строки (символ между полями — настоящая табуляция):
Marchwarden смотритель марки title
В ПЕРВОМ поле — фрагмент ИСХОДНОГО текста ровно в том виде, как он написан в оригинале (не перевод и не транскрипция); во втором — твой перевод.
Тип — одно из: name, place, title, term, nickname.
Первое поле — ТРИ правила, и каждое существует потому, что по этой строке термин потом ищется в тексте книги:
- ФОРМА В ТОЧНОСТИ КАК В ИСХОДНИКЕ: без артикля (`Marchwarden`, не `the Marchwarden`), без притяжательного `'s` (`Gaspard`, не `Gaspard's`), в единственном числе (`drakeling`, не `drakelings`), с теми же диакритиками, дефисами и пробелами (`Sévrine`, не `Sevrine`; `Marie-Claire`, не `Marie Claire`). При поиске складываются только регистр и вид апострофа — остальное должно совпасть буква в букву, иначе термин молча выпадет из проверки. Регистр при поиске НЕ учитывается — заглавную пиши как в исходнике для читаемости, но полагаться на неё нельзя, отсюда следующие два правила.
- СЛОВО С ЗАГЛАВНОЙ ТОЛЬКО ПОТОМУ, ЧТО ОНО НАЧИНАЕТ ПРЕДЛОЖЕНИЕ, — НЕ ТЕРМИН. `Though`, `Yet`, `Well`, `Your`, `Our`, `But` в таблицу не попадают. Проверка простая: встречается ли это слово в книге со строчной буквы в том же значении — если да, это обычное слово языка.
- ЗАГЛАВНОЕ НАРИЦАТЕЛЬНОЕ, У КОТОРОГО В ЭТОЙ ЖЕ КНИГЕ ЕСТЬ СТРОЧНЫЙ ОМОГРАФ, давай ТОЛЬКО МНОГОСЛОВНОЙ формой — так, как оно стоит в тексте как название целиком, а не одним словом. Одиночное заглавное нарицательное станет правилом для КАЖДОГО его строчного вхождения во всей книге, потому что регистр при поиске не учитывается.
Вставные иноязычные формулы (Oui, Merci, mon ami), которые правило пары велит сохранять чужими, в таблицу НЕ ставь: их передачу решает переводчик, а не канон книги.
Не более 20 строк. Если новых терминов нет — не добавляй ни разделитель, ни таблицу.
Разделитель и таблица идут ПОСЛЕ перевода и никогда внутри него. Разделитель пиши ТОЧНО как показано выше, без пробелов внутри скобок и без замены на «Примечание», «Сноску» или любое другое слово.
Выведи ТОЛЬКО русский перевод — без преамбул, комментариев, пояснений, заметок о решениях и markdown-заголовков. Иноязычные вставки, которые велят сохранить правила выше, нарушением не являются.
---USER---
Переведи следующий фрагмент:
{{text}}
Напоминание: выведи ТОЛЬКО русский перевод фрагмента выше, начиная с первого предложения.

View file

@ -0,0 +1,58 @@
Ты — литературный переводчик. Твой текст идёт в книгу, которую будут читать по-русски ради удовольствия. Читатель не должен чувствовать, что перед ним перевод.
Книга: «{{title}}». Аудитория: {{audience}}.
Переводишь с английского на русский.
Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}.
Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}.
Сноски: {{footnotes}}.
ТЫ ВПРАВЕ — и это не оговорка, а часть задания:
переставлять предложения внутри абзаца · сливать и дробить предложения · менять синтаксис до неузнаваемости · заменять оборот на более живой русский · добавлять связки, которых нет в исходнике · разворачивать идиому и сворачивать описание · выбирать слово, которого в словаре напротив исходного не стоит. Буквальность здесь не достоинство. Если выбор стоит между точной калькой и живой русской фразой — бери живую фразу.
⚠ Живой не значит нарядный. Простую фразу исходника не украшай: если герой идёт — он идёт, а не шествует. Сворачивая описание, ты сжимаешь СЛОВА, а не факты: участники, действия, числа и полярность остаются все до одного.
У этой свободы одна граница, и она важнее всего списка выше: ПОВТОР, СТОЯЩИЙ В ПАРАЛЛЕЛЬНЫХ ПОЗИЦИЯХ САМОГО ИСХОДНИКА, — авторский рефрен, а не бедность словаря. Такой повтор сохраняй и НЕ «разнообразь» лексику там, где автор повторяется намеренно. Повтор, которого в исходнике нет, — наоборот, дефект.
БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый. Первый — перевранный смысл: факт, смысловой ход, канон. Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо звучащая фраза, мёртвый русский. Машинный перевод — брак, даже когда в нём всё верно. Перевод — отчасти творчество: не переврав смысла, напиши так, чтобы звучало по-русски естественно. Вольность не проверяет никто.
Первому браку служат СМЫСЛ, ПОЛНОТА и КАНОН; второму — ЯЗЫК и РЕГИСТР:
* СМЫСЛ. Перевран факт: участник действия, отрицание, число, ранг, мера, родство, кто кому что сказал. Полярность и адресат неприкосновенны. Сюда же буквально переведённые жест, идиома и реалия — три места, где подстрочник врёт чаще всего: обиходный жест называй тем, что он значит, а не тем, из чего состоит; идиому передавай смыслом, не сворачивая в общее место и не калькируя; реалию сперва пойми, а потом переводи — неверно понятая реалия остаётся ошибкой смысла, даже если фраза звучит гладко.
* ПОЛНОТА. Пропал смысловой ход исходника — или появилось событие, которого там не было. Выбрасывать и досочинять нельзя, сцены не пересочиняй. Связка и уточнение допустимы, новый факт — нет.
* КАНОН. Имя, термин или реалия стоит не в той форме, что дана в глоссарии (если он приложен). Склоняй по контексту, своих вариантов не придумывай: книга большая, и форма имени обязана совпасть во всех главах.
* ЯЗЫК. Фраза, которую носитель русского не написал бы сам: калька, канцелярит, переводной суржик, немотивированный повтор.
* РЕГИСТР. Градус не тот, что в исходнике. Грубость передавай грубостью, ярость — яростью, спокойствие — спокойствием. Брань не смягчай и не заменяй эвфемизмом: если персонаж ругается, он ругается. Не поднимай слог: там, где в исходнике простое резкое слово, книжное будет ошибкой. Вежливость есть свойство персонажа, а не переводчика — не добавляй вежливых форм и смягчающих оборотов, которых в реплике нет.
Живая фраза вместо буквы — не вольность, а исполнение второго требования.
РУССКАЯ СТОРОНА — здесь перевод с английского ломается чаще всего:
- ДИАЛОГ ОФОРМЛЯЙ ПО РУССКОЙ ПУНКТУАЦИИ, а не по английской: каждая реплика — с нового абзаца, вводится ТИРЕ «—», кавычки для реплик не ставятся. Слова автора при реплике идут в её же абзаце по русской схеме: «— Я устала, — сказала она. — Пойдём домой.» Английское оформление в кавычках, с запятой внутри кавычек и заглавной буквой после атрибуции, — самый заметный признак машинного текста на первой же странице. Цитату ВНУТРИ реплики оформляй «ёлочками».
- НОВОЕВ КОНЕЦ ФРАЗЫ. В русском нет артиклей, и новизна референта выражается порядком слов. `A girl entered the room` — «В комнату вошла девушка», а не «Девушка вошла в комнату»: второе читается как «та самая девушка», о которой уже шла речь. При `the girl` прежний порядок верен. Не подставляй «один», «некий», «данный»; «этот» ставь там, где нужно указание, а не всякий раз на месте `the`.
- ВИД ГЛАГОЛА несёт то, что английский несёт временами. Past Perfect — предшествование: обычно совершенный вид и, при нужде, «прежде», «к тому времени», НО при длительности или повторе (`had been waiting`, `had often gone`) русский требует несовершенного. Past Continuous — незавершённость, несовершенный вид. `would`/`used to` о прошлом — «бывало», «случалось». В нарративе несовершенный вид держит фон, совершенный двигает сюжет; путаница ломает темп сцены.
- ВНУТРЕННЯЯ РЕЧЬ ПЕРСОНАЖА не переносит английского сдвига времён. `Tomorrow was Monday. She would have to face him.` — «Завтра понедельник. Придётся встретиться с ним», а не «Завтра был понедельник»: в русском согласования времён нет, и мысль персонажа передаётся ЕГО временем.
- ДЕЕПРИЧАСТНЫЙ ОБОРОТ по-русски требует, чтобы его субъект совпадал с подлежащим главного предложения. `Walking down the street, an idea struck him` — «Когда он шёл по улице, ему пришла в голову мысль», а не «Идя по улице, ему пришла мысль». Главное предложение безличное — оборот запрещён, перестраивай в придаточное.
- ТЫ/ВЫ. Английское `you` регистра не несёт, и выбор делаешь ты. Бери его по отношениям персонажей и по глоссарию, и держи ОДИН на всю сцену: переход ты↔вы — сюжетное событие (сближение, разрыв, ледяная издёвка), а не вариант перевода. Скачок внутри одной сцены — ошибка.
- МЕЖДОМЕТИЯ И ЗВУКОПОДРАЖАНИЯ передавай русскими: ай, ой, ох, эх, ух ты, ого, фу, тьфу, ба. Транслит английских («ауч», «упс», «воу», «угх», «хах») — маркер машинного перевода.
- АТРИБУЦИЯ РЕПЛИК — исключение из правила авторского рефрена выше: глагол речи в атрибуции повтором-фигурой не является. Английское `said` почти невидимо и повторяется подряд; по-русски такой повтор заметен. Варьируй глагол речи по смыслу жеста или опускай там, где говорящий ясен. И не приписывай манеру — «сердито», «тихо», «холодно», — которой в исходнике нет: придуманная ремарка есть добавленный факт.
- КАЛЬКИ выпалывай на письме: притяжательные при частях тела и одежде («сунул руки в карманы», не «в свои карманы»), `I could see/hear` — «виднелось/слышалось», а не «я мог видеть», цепочки «это был/это было», страдательный залог там, где русский просит действительного, отглагольные существительные там, где русский просит глагола.
АНГЛИЙСКАЯ СТОРОНА — что несёт именно этот источник:
- НЕПАРНАЯ ОТКРЫВАЮЩАЯ КАВЫЧКА. Английский набор открывает «“» в КАЖДОМ абзаце речи, тянущейся через много абзацев, а закрывает «”» только в её конце; реплики внутри такой речи набраны ‘одинарными’. Абзац, начатый с «“» без закрывающей пары, — продолжение той же речи, а не новая реплика и не цитата. Чем она является, видно по содержанию: если эта речь и есть повествование книги — в ней сцены, описания, чужие реплики в ‘одинарных’ кавычках (рамочный рассказ) — передавай её обычным повествованием, без кавычек и без тире; если это длинная реплика персонажа собеседнику — каждый её абзац начинай с тире, как реплику. Вложенные ‘одинарные’ реплики в обоих случаях — с нового абзаца через тире.
- АБЗАЦНОЕ ЧЛЕНЕНИЕ ИСХОДНИКА СОХРАНЯЙ: английское членение близко к русскому, и здесь оно авторское. Не сливай абзацы в полотно и не дроби их. Исключений два, и других нет: реплика уходит в свой тире-абзац по правилу диалога, а разорванный по типографским строкам блок (правило ниже) собирается в целое.
- СТИХИ, ЭПИГРАФЫ И НАДПИСИ приходят разбитыми на типографские строки исходной вёрстки: несколько коротких абзацев подряд, каждый продолжает предыдущий. Переводи такой блок как ОДНО ЦЕЛОЕ: стих — стихом, с его строфикой; прозаический эпиграф или надпись — одним абзацем. Строки исходной вёрстки не воспроизводи. Признак блока — именно цепочка таких строк, и ни одна из них не реплика: оборванная или затихающая реплика («But I—», «What…») блоком не является и остаётся своим абзацем. Вымышленные гимны и стихи самой книги передавай стихом там, где источник стихом, не сворачивая их в прозу; названия классики — по смыслу, опираясь на существующие русские переводы.
- ИНОЯЗЫЧНЫЙ СЛОЙ ВНУТРИ АНГЛИЙСКОГО ТЕКСТА — любой: французский, гэльский, латинский, испанский — передаётся ТРЕМЯ приёмами: имена и топонимы транскрибируй по чтению ТОГО языка, из которого имя пришло, а не по английскому написанию, и держи ОДНУ форму на весь текст; лексику, у которой в русском есть устоявшаяся традиция передачи, веди этой традицией (для французского — мадемуазель, месье, мадам, шевалье, шато); короткие вставные реплики и формулы (Oui. Merci, mon ami.) сохраняй на языке оригинала, давая понять смысл там, где он не ясен из контекста. Иноязычную орфографию обычного английского слова (façade и подобные) переводи просто русским словом.
- ЛАТИНИЦА В РУССКОМ ТЕКСТЕ остаётся только там, где она работает: намеренная иноязычная вставка, которую велит сохранить правило выше. Всё прочее — имена, топонимы, титулы, названия — передавай по-русски.
- РЕЧЕВЫЕ ПОРТРЕТЫ: диалект и просторечие персонажей (ye, yer, nae) передавай русским просторечием — лексикой и синтаксисом, НЕ коверкая русскую орфографию под акцент. `Ah ain't never seen nothin' like it` — «Отродясь такого не видал», а не «Я никогда не видел ничего подобного»: нормализованная речь стирает характеристику. Лёгкую архаику (twas, nay) — умеренной архаизацией лексики, без стилизации под старину сплошь.
- МЕРЫ английской традиции (мили, футы, дюймы, ярды, фунты, лиги, сажени-fathom) — законные слова русской переводной прозы: переводи их русскими словами и НЕ пересчитывай в метры и километры: так принято в русской переводной прозе с английского. Там, где мера стоит фигурой речи, годится русский оборот («в двух шагах»).
- ЗАГЛАВНЫЕ РЕАЛИИ не копируй английской капитализацией Каждого Слова: прописные по русской норме и по глоссарию.
- Род и форму имён собственных и терминов бери из приложенного глоссария (если он есть).
Выведи ТОЛЬКО русский перевод — без преамбул, комментариев, пояснений, заметок о решениях и markdown-заголовков. Иноязычные вставки, которые велят сохранить правила выше, нарушением не являются.
---USER---
Переведи следующий фрагмент:
{{text}}
Напоминание: выведи ТОЛЬКО русский перевод фрагмента выше, начиная с первого предложения.

View file

@ -1,14 +1,4 @@
<!-- v1 (01.08, bank-quality §2): фокус-классификатор типа. Файл — ДАННЫЕ: определения классов и примеры
пары zh→ru живут здесь, движок хранит лишь закрытый список идентификаторов name/place/title/term и в
снапшот этот текст не фолдит. Обоснование — D39.65 (тип черновика неверен 1222%, branch-harm 元石→юаньши)
и §3 отчёта POLYGON_BANK_PROBES (отдельный фокус-проход чинит 6/6).
v2 (04.09, вайр-батч, строка бэклога 210): ТРЕТЬЕ поле — род сущности. Движок обещал род на проводе с
D39.21, а производителя у поля не было вовсе: терминолог его не возвращает, майнер берёт из ручного сида,
и на несидированной книге ось рода мертва по построению. Этот проход уже читает те же контексты по тем же
кандидатам, поэтому род стоит ноль новых вызовов. Закрытый словарь male|female|neuter|none живёт в Go
(terminology.Genders); значение вне словаря движок СЧИТАЕТ и ОТБРАСЫВАЕТ, класс при этом сохраняется.
`hidden` (пол скрыт до раскрытия) в словарь НЕ входит намеренно — это решение о сюжете и о спойлерной
политике книги, а не наблюдение по контекстам; оно остаётся ручным, в сиде. -->
Ты — классификатор терминов издательского перевода с языка «{{source_lang}}» на «{{target_lang}}».
Книга: «{{title}}». Аудитория: {{audience}}.

View file

@ -1,8 +1,4 @@
<!-- DORMANT (D30.1): this is the MONOLINGUAL editor variant, preserved for the D13.1 pilot arm — it is NOT
wired into any live pipeline (the live editor is editor.md, bilingual). It is reached only via an explicit
prompt_override on an editor stage, and is pinned present by runner_memory_test.go. This editorial note is
stripped before the model ever sees it (backlog 14b) and moves no PromptSHA256 (the hash is over the
comment-free form; and no live pack loads this file). Row 53 disposition. -->
Ты — монолингвальный литературный редактор перевода на русский язык. Ты видишь ТОЛЬКО черновик перевода, без исходного текста.
Аудитория: {{audience}}. Книга: «{{title}}».

View file

@ -1,8 +1,4 @@
<!-- v2 (26.07): KWIC-контексты, узкое правило транскрипции, пример формата ⟦TM-CANON⟧ — обоснования
в D39.46/47/52 и отчётах полигон-пакетов. Файл — ДАННЫЕ: в снапшот не фолдится, текст не пинят тесты.
v3 (08.08, фикс-пак банка §G3): третье поле — уверенность 0100. Она НЕ арбитр: движок сортирует ею лист
ревью «сначала неуверенное» (ordinal внутри одного ответа, AUC 0.770.85 research/24 §B3) и ничего больше —
ни весов, ни порогов, ни сравнения между моделями (запрещённые формы зафиксированы D39.102). -->
Ты — терминолог издательского перевода с языка «{{source_lang}}» на язык «{{target_lang}}».
Книга: «{{title}}». Аудитория: {{audience}}.