Freeze polygon pre-registration and rig for the Q4b diff-editor arm before any paid call

This commit is contained in:
heaven 2026-08-05 00:34:40 +03:00
parent 6a0233ff53
commit 52e12f311e
5 changed files with 591 additions and 0 deletions

View file

@ -0,0 +1,129 @@
# 19. Контракт редактора: ДИФФ против FULL-REGEN (арм Q4b) + покрытие инъекции
**Полигон-сессия 0405.08.2026.** Расширение зоны по слову владельца 05.08: «довести до конца
найденный пиздец, доказать на конкретике, как пользоваться редакторами правильно».
Носитель — **строка 106 бэклога**: «Слой 3 НЕ достроен: контракт редактора = full-regen, а целевой —
узкие мандаты + ДИФФЫ». Арм **Q4b** пре-регистрирован в `15-segmentation-empirics.md:108` и
**не прогонялся НИ РАЗУ**. Зона: `eval/editor_contract/` + этот файл + пинг в PROGRESS «Полигон».
Потолок пробы **$0.40**. Не коммичу, кроме пре-рег-фриза.
## §0. Что уже установлено — и почему это не закрывает вопрос
Проба 18 (`18-editor-wire-probe.md`) на боевом проводе `deepseek-v4-pro`:
единый закон-блок исполняется 21/21 клеток; неверная строка банка принимается 6/6 и вытесняет
верную 5/6; редактор без блока ломает 512 из 20 канонических клеток черновика (два розыгрыша);
и он же вносит собственные дефекты (тихий обрыв при `finish=stop`, 五百年→«полувекового»).
**Чего проба 18 не могла сказать:** является ли это свойством МОДЕЛИ или свойством КОНТРАКТА
роли. Действующий контракт — full-regen с обязательной ДИСКУРС-ПЕРЕВЁРСТКОЙ и прямым мандатом
«варьируй лексику» (`editor.md`), то есть модель просят переписать текст целиком. Целевой контракт
(`09-target-architecture.md` §Слой 3, спека `research/19` §C1C2) — узкие правки диффом. Разницу
никто не мерил.
### §0.1 $0-находка ДО пробы: дыра пайплайновая, а не редакторская
Инъекция селективна и ограничена бюджетом 800 токенов (`pipeline-c1.yaml`), поэтому блок несёт
только сработавшее в чанке. Замер по уже оплаченному сырью пробы 18 (`editor_contract/offblock.py`;
знаменатель — 41 клетка: ПОДПИСАННЫЕ строки голда, встречающиеся в исходнике окна и отсутствующие
в блоке; 18 уникальных сущностей; `转`/`炼化` не скорятся — нет устойчивого стема):
| | канон подписанных ВНЕ блока |
|---|---|
| **черновик** (вход редактора) | **18/41 (44%)** |
| редактор full-regen БЕЗ блока (A0 · A5) | 16/41 · 17/41 |
| редактор full-regen + закон-блок (A1) | 24/41 |
**Разброс армов 16…24 лежит ВНУТРИ дисперсии одиночного розыгрыша, измеренной той же пробой
(7 клеток из 21) ⇒ утверждать «редактор ломает подписанные сущности вне блока» эти данные НЕ
позволяют.** Что они позволяют утверждать твёрдо: **канон по подписанным строкам вне
инъецированного подмножества = 44% уже в ЧЕРНОВИКЕ.** `花酒行者` («Монах Цветочного Вина») пришёл
неканоническим из транслятора — редактор его не ломал. Дыра общая для обеих ролей, потому что
инъекция селективна у обеих.
⇒ Осей две: **контракт правки** (эта проба) и **покрытие инъекции** (шире пробы; вход для владельца).
---
## §1. ПРЕ-РЕГИСТРАЦИЯ (записано ДО первого платного вызова)
Дата фиксации: 2026-08-05, до любых платных вызовов этой пробы.
### 1.1 Материал (переиспользован из пробы 18, $0)
Те же 6 окон `pick_windows()`; те же черновики арма B транслятора (`inj-w{k}-B.json`); те же 12
посаженных дефектов (`DEFECTS`, дословный список — §0.5 пробы 18). Прямая сравнимость с уже
оплаченными армами A1/A5 — по построению.
### 1.2 Армы (60 вызовов `deepseek-v4-pro`, провод = edit-стадия c1)
| Арм | Вызовов | Контракт | Блок | Черновик |
|---|---|---|---|---|
| **D1** | 18 (6×3) | ДИФФ | закон-блок §B | чистый |
| **D2** | 18 (6×3) | ДИФФ | нет | с посадками |
| **F1** | 12 (6×2) | full-regen | закон-блок §B | чистый |
| **F5** | 12 (6×2) | full-regen | нет | с посадками |
F1/F5 — ДОБОР реплик до N=3: у пробы 18 по одному розыгрышу (A1, A5), а её собственная находка —
одиночный розыгрыш нельзя цитировать как величину. Реплики различаются только недетерминизмом
провайдера (`temperature` в thinking-режиме DeepSeek игнорируется, quirks 00).
### 1.3 Дифф-контракт (спека `research/19` §C2, реализация `editor_contract/`)
Промпт `prompts/editor-diff.md`: мандаты верности/терминов/стиля из `editor.md` СОХРАНЕНЫ,
**переверстка ИСКЛЮЧЕНА** (по §C1/§C3 reflow — отдельный проход, арм Q4c), «пустой список правок —
полноценный ответ» (`NO_CHANGE`) — дисциплина do-nothing из §C3.
Формат — anchored search/replace, без номеров строк. Apply (`apply.py`, детерминированный,
самопроверен 9/9 юнит-кейсами до первого платного вызова): нормализация пробелов и классов
пунктуации («»""„ · —–- · … · '); якорь не найден → REJECT; **якорь неоднозначен (≥2 совпадений)
→ REJECT, а не тихое применение первого**; пересечение спанов → REJECT; малформед → операции нет.
Отброшенная операция оставляет черновик как есть — blast-radius одной правки, а не чанка.
### 1.4 Метрики (все детерминированные, $0; судейского рига НЕТ и он вне скоупа)
1. **Формат-комплаенс** = applied / ops_total по всем дифф-вызовам. Плюс доли reject по причинам и
доля `NO_CHANGE`/малформед.
2. **Канон В блоке** — 21 клетка, метод пробы 18 (с развязкой конфаунда 真元/元海).
3. **Канон ВНЕ блока** — 41 клетка, §0.1.
4. **Фикс-рейт посадок** — 12 дефектов, метод пробы 18 (regex + обязательный глаз).
5. **Атом-omission** ($0, детерминированно): по каждому предложению исходника — присутствие
многозначных ЧИСЕЛ и отрицательной полярности где угодно в выходе. Не длино-зависимо.
6. **Внесённые дефекты**: обвал непробельной длины >40% против черновика и числовой дрейф
(форма regression_guard); плюс глазная вычитка.
7. **Деньги и выходные токены** — COGS-ось (выход редактора ≈ 90% COGS, `research/15`).
### 1.5 Критерии (стоп-гейт Q4b дословно из `exp15:147`, пороги не мои)
> «Q4b: формат-комплаенс <90% ИЛИ fidelity-first trap хуже full-regen диффы откладываются
> (закрыть D21.4/D21.10 явно); ≥90% и атом-omission=0 → дифф-редактор в прод-кандидаты.»
Операционализация, фиксируется здесь:
- **комплаенс** — п.1 ≥ 0.90;
- **fidelity-first trap** — п.4 (фикс-рейт посадок) дифф-арма НЕ ХУЖЕ full-regen-арма на тех же
посадках; знаменатель 12, оба арма усредняются по репликам;
- **атом-omission = 0** — п.5 на дифф-армах;
- канон (пп.23) и COGS (п.7) — **сопутствующие**, порогов не имеют, печатаются числом.
- Средние исходы на N=6×3 печатаются как «неопределённо», не абсорбируются.
### 1.6 Смета (напечатана ДО первого платного вызова)
`probe.py --plan`: 60 вызовов (36 дифф + 24 full-regen), вход ≈114 265 ток. → $0.0497.
Выход: ожидаемо 105 600 ток. → **ИТОГО $0.1416**; пессимистично 336 000 ток. → $0.3420;
абсолютный потолок (все 60 в cap 16000) $0.8849 — недостижим: перед КАЖДЫМ вызовом стоит
**проекционный гард** (не вызываем, если накопленное + худший исход вызова пробьёт $0.40),
плюс живой стоп $0.35.
### 1.7 Границы вывода, объявленные заранее
- Проза и переверстка НЕ сравниваются: дифф-арм по спеке их не делает. Вывод «дифф лучше/хуже»
относится к верности, терминам и деньгам, не к художественности.
- Судейского рига нет — никаких клеймов о качестве, которые требуют судьи.
- Одна книга, одна пара, 6 окон, единица ~500 ру-токенов выхода (боевой потолок 3200).
- Модель одна — `deepseek-v4-pro`. `research/19` предупреждает: слабые модели на edit-форматах
РАБОТАЮТ ХУЖЕ, чем отдавая текст целиком (aider: GPT-3.5 46% whole против 30% diff), и вся
эмпирика форматов правок снята на КОДЕ, на прозе не мерена. Отрицательный исход ожидаем и легитимен.
---
## §2. Результаты
*(заполняется после прогона)*

View file

@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Толерантный детерминированный apply anchored search/replace — Python-порт Go-стиля,
по спеке `docs/research/19-chunking-cohesion.md` §C2 (риг арма Q4b, `exp15:108`).
Спека дословно: якорь ищется с нормализацией пробелов/пунктуационных вариантов; неоднозначный
якорь (2 совпадений) REJECT операции (не тихое применение первого); якорь не найден REJECT;
reject-rate телеметрия. Малформед 1 регенерация далее операция ОТБРАСЫВАЕТСЯ (черновик
остаётся) blast-radius одной правки, а не чанка.
"""
from __future__ import annotations
import re
import unicodedata
from dataclasses import dataclass, field
OP_RE = re.compile(
r"<{5,9}\s*SEARCH\s*\n(?P<search>.*?)\n={5,9}\s*\n(?P<replace>.*?)\n>{5,9}\s*REPLACE",
re.S)
NO_CHANGE = "NO_CHANGE"
# Пунктуационные варианты, которые модель законно путает при копировании якоря.
PUNCT_CLASSES = [
("«»\"“”„‟", '"'),
("—–‒−-", "-"),
("", "..."),
("'`", "'"),
]
def fold(s: str) -> str:
"""Нормализация ДЛЯ ПОИСКА (не для вывода): NFKC · схлоп пробелов · классы пунктуации."""
s = unicodedata.normalize("NFKC", s)
for chars, to in PUNCT_CLASSES:
for ch in chars:
s = s.replace(ch, to)
return re.sub(r"\s+", " ", s).strip()
def _fold_map(text: str) -> tuple[str, list[int]]:
"""Свёрнутый текст + карта «индекс в свёрнутом → индекс в оригинале»."""
out, idx = [], []
prev_space = True # схлоп ведущих пробелов
for i, ch in enumerate(unicodedata.normalize("NFKC", text)):
if ch.isspace():
if prev_space:
continue
out.append(" ")
idx.append(i)
prev_space = True
continue
prev_space = False
for chars, to in PUNCT_CLASSES:
if ch in chars:
ch = to
break
if len(ch) == 1:
out.append(ch)
idx.append(i)
else: # … → ...
for c in ch:
out.append(c)
idx.append(i)
return "".join(out), idx
@dataclass
class ApplyResult:
text: str
ops_total: int = 0
applied: int = 0
rejected_notfound: int = 0
rejected_ambiguous: int = 0
rejected_overlap: int = 0
rejected_empty: int = 0
malformed: bool = False
no_change: bool = False
details: list[str] = field(default_factory=list)
@property
def rejected(self) -> int:
return (self.rejected_notfound + self.rejected_ambiguous
+ self.rejected_overlap + self.rejected_empty)
@property
def compliance(self) -> float:
"""Доля операций, доехавших до применения. Стоп-гейт Q4b: ≥0.90."""
return self.applied / self.ops_total if self.ops_total else 1.0
def parse_ops(reply: str) -> tuple[list[tuple[str, str]], bool]:
"""→ ([(search, replace)], malformed). NO_CHANGE = пустой список, не малформед."""
body = reply.strip()
ops = [(m.group("search"), m.group("replace")) for m in OP_RE.finditer(body)]
if ops:
# мусор ВНЕ операций (преамбулы/комментарии) — нарушение правила 6, но операции валидны:
# считаем малформедом только если разметка вообще не распозналась
return ops, False
if NO_CHANGE in body and len(body) < 200:
return [], False
return [], True
def apply_ops(draft: str, reply: str) -> ApplyResult:
ops, malformed = parse_ops(reply)
res = ApplyResult(text=draft, malformed=malformed, no_change=(not ops and not malformed))
if malformed or not ops:
return res
res.ops_total = len(ops)
folded, idx = _fold_map(draft)
spans: list[tuple[int, int, str]] = []
for search, replace in ops:
key = fold(search)
if not key:
res.rejected_empty += 1
res.details.append("REJECT пустой якорь")
continue
hits = [m.start() for m in re.finditer(re.escape(key), folded)]
if not hits:
res.rejected_notfound += 1
res.details.append(f"REJECT якорь не найден: {search[:60]!r}")
continue
if len(hits) > 1:
res.rejected_ambiguous += 1
res.details.append(f"REJECT якорь неоднозначен ({len(hits)}): {search[:60]!r}")
continue
s = idx[hits[0]]
e_fold = hits[0] + len(key) - 1
e = idx[e_fold] + 1
spans.append((s, e, replace))
# пересечения: операция, перекрывающая уже принятую, отбрасывается (спека: не пересекаются)
spans.sort()
accepted: list[tuple[int, int, str]] = []
for s, e, r in spans:
if accepted and s < accepted[-1][1]:
res.rejected_overlap += 1
res.details.append(f"REJECT пересечение спанов на {s}")
continue
accepted.append((s, e, r))
out = draft
for s, e, r in sorted(accepted, reverse=True): # с хвоста — офсеты не едут
out = out[:s] + r + out[e:]
res.text = out
res.applied = len(accepted)
return res

View file

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Метрика «дрейф ПОДПИСАННЫХ сущностей ВНЕ инъецированного блока» — класс 花酒行者.
Найдена пробой 18 (`docs/experiments/18-editor-wire-probe.md`): инъекция селективна и ограничена
бюджетом 800 токенов, поэтому блок несёт лишь то, что сработало в чанке. Всё остальное подписанное
не защищено ничем а редактор его переписывает. Здесь это считается числом.
Знаменатель ПОДПИСАННЫЕ (approved, `gold.jsonl` in_bank) строки, чей src встречается в исходнике
окна и которых НЕТ в тест-блоке пробы 18. Скорится присутствие канонической формы в выходе.
$0: работает по уже персистированному сырью.
"""
from __future__ import annotations
import json
import re
import sys
import unicodedata
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
from inject_probe import pick_windows, TERMS # noqa: E402
GOLD = {r["bank_src"]: r["gold_dst"]
for r in (json.loads(l) for l in open(REPO / "eval/bank_arbitration/gold/gold.jsonl", encoding="utf-8"))
if r.get("in_bank")}
# Стем-регексы канонических форм. КОНСЕРВАТИВНО: скорим только строки, у которых форма
# опознаётся устойчивым стемом. Отброшенные строки печатаются — знаменатель честный.
STEM = {
"方源": r"фан\s*юан",
"方正": r"фан\s*чжэн",
"古月": r"гуюэ", # «Гу Юэ» раздельно НЕ матчится — это и есть дрейф
"古月漠北": r"гуюэ\s*мобэй",
"蛊虫": r"гу-черв",
"本命蛊": r"гу\s*жизни",
"希望蛊": r"гу\s*надежды",
"酒虫": r"винн\w*\s*черв",
"家老": r"старейшин",
"花酒行者": r"монах\s*цветочного\s*вина",
"青茅山": r"гор\w*\s*цинмао",
"凡人": r"смертн",
"一转": r"перв\w*\s*ранг",
"三转": r"трет\w*\s*ранг",
"甲等": r"класс\w*\s+а\b",
"乙等": r"класс\w*\s+б\b",
"丙等": r"класс\w*\s+в\b",
"丁等": r"класс\w*\s+г\b",
}
# Не скорятся: 转 «ранг» (слишком общая форма — ложные срабатывания внутри «первый ранг»),
# 炼化 «очистить и подчинить» (глагольная перифраза, устойчивого стема нет).
DROPPED = {"", "炼化"}
def norm(s: str) -> str:
return " ".join(unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е").split())
def offblock_cells(block_terms: set[str] | None = None):
"""[(окно, src, gold_dst, regex)] — подписанные строки окна вне блока."""
block = set(TERMS) if block_terms is None else block_terms
out = []
for k, (_, buf, _) in enumerate(pick_windows()):
for src, dst in GOLD.items():
if src in block or src in DROPPED or src not in STEM:
continue
if src in buf:
out.append((k, src, dst, STEM[src]))
return out
def score(tags: dict[str, str], label_of=lambda k: None):
"""tags: {метка арма: шаблон тега с {k}}. Печатает долю канонических форм вне блока."""
cells = offblock_cells()
print(f"знаменатель: {len(cells)} клеток (подписанные строки в исходнике окна, ВНЕ блока), "
f"{len({c[1] for c in cells})} уникальных сущностей")
print(f"не скорятся (нет устойчивого стема): {', '.join(DROPPED)}\n")
rows = {}
for arm, pat in tags.items():
hit = tot = 0
misses = []
for k, src, dst, rx in cells:
f = RAW / (pat.format(k=k) + ".json")
if not f.exists():
continue
out = norm(json.load(open(f, encoding="utf-8"))["content"])
tot += 1
if re.search(rx, out):
hit += 1
else:
misses.append(f"w{k}:{src}«{dst}»")
rows[arm] = (hit, tot, misses)
pct = f"{100*hit/tot:.0f}%" if tot else ""
print(f" {arm:34s} {hit:3d}/{tot:<3d} ({pct})")
return rows
if __name__ == "__main__":
print("=== ЧЕРНОВИК (что редактор получил на вход) ===")
cells = offblock_cells()
hit = 0
dmiss = []
for k, src, dst, rx in cells:
d = norm(json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))["content"])
if re.search(rx, d):
hit += 1
else:
dmiss.append(f"w{k}:{src}«{dst}»")
print(f" канон в черновике: {hit}/{len(cells)}")
print(f" чего в черновике не было: {', '.join(dmiss)}\n")
print("=== ВЫХОД РЕДАКТОРА ===")
rows = score({
"A0 full-regen БЕЗ блока": "edp-A0-w{k}",
"A5 full-regen БЕЗ блока (2-й розыгр.)": "edp-A5-w{k}",
"A1 full-regen + закон-блок": "edp-A1-w{k}",
"A2 full-regen + блок (неверная 元海)": "edp-A2-w{k}",
})
print("\n=== что именно потеряно (арм A1, блок есть, но эти строки в него не попали) ===")
for m in rows["A1 full-regen + закон-блок"][2]:
print(" ", m)

View file

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Арм Q4b (`docs/experiments/15-segmentation-empirics.md:108`, спека `research/19` §C1C2):
ДИФФ-редактор (anchored search/replace + детерминированный толерантный apply) против действующего
FULL-REGEN редактора. Арм пре-регистрирован в exp15 и не гонялся НИ РАЗУ; носитель строка 106
бэклога («Слой 3 НЕ достроен: контракт редактора = full-regen, а целевой узкие мандаты + ДИФФЫ»).
Материал переиспользован из пробы 18 ($0): те же 6 окон, те же черновики арма B транслятора, те же
12 посаженных дефектов. Модель боевой редактор deepseek-v4-pro, провод как edit-стадия c1.
Reflow в дифф-арм НЕ входит: по §C1/§C3 он ОТДЕЛЬНЫЙ проход (арм Q4c) проза/переверстка этой
пробой НЕ сравниваются, и это граница вывода, а не упущение.
Сырьё durable ~/books/gu-zhenren/bank-arbitration/, префикс ec-*.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
REPO = Path("/home/ubuntu/projects/textmachine")
HERE = Path(__file__).resolve().parent
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
load_dotenv(REPO / "eval" / ".env")
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
import editor_wire_probe as P # noqa: E402 провод/окна/черновики/посадки пробы 18
from apply import apply_ops # noqa: E402
from inject_probe import pick_windows, TERMS # noqa: E402
DIFF_PROMPT = HERE / "prompts" / "editor-diff.md"
CEILING_USD = 0.40
HARD_STOP_USD = 0.35
REPLICATES = 3
def render_diff(text: str, draft: str) -> tuple[str, str]:
canon = P.strip_comments(DIFF_PROMPT.read_text(encoding="utf-8"))
sys_part, user = canon.split(P.USER_SEP, 1)
return P.render(sys_part.strip(), text, draft), P.render(user.strip(), text, draft)
def messages(kind: str, text: str, draft: str, block: str | None) -> list[dict]:
if kind == "full":
return P.editor_messages(text, draft, block)
system, user = render_diff(text, draft)
msgs = [{"role": "system", "content": system}]
if block and block.strip():
msgs.append({"role": "system", "content": block})
msgs.append({"role": "user", "content": user})
return msgs
def plan_arms():
"""[(tag, kind, window, messages, draft)] — армы в порядке возрастания риска."""
wins = pick_windows()
out = []
for rep in range(1, REPLICATES + 1):
for k, (_, buf, terms) in enumerate(wins):
draft = P.draft_of(k)
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
out.append((f"ec-D1-w{k}-r{rep}", "diff", k, messages("diff", buf, draft, blk), draft))
for k, (_, buf, terms) in enumerate(wins):
draft, _ = P.planted_draft(k, P.draft_of(k))
out.append((f"ec-D2-w{k}-r{rep}", "diff", k, messages("diff", buf, draft, None), draft))
# добор реплик действующего контракта: у пробы 18 по одному розыгрышу (A1 и A5),
# а собственная находка той пробы — одиночный розыгрыш нельзя цитировать как величину.
for rep in (2, 3):
for k, (_, buf, terms) in enumerate(wins):
draft = P.draft_of(k)
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
out.append((f"ec-F1-w{k}-r{rep}", "full", k, messages("full", buf, draft, blk), draft))
for k, (_, buf, terms) in enumerate(wins):
draft, _ = P.planted_draft(k, P.draft_of(k))
out.append((f"ec-F5-w{k}-r{rep}", "full", k, messages("full", buf, draft, None), draft))
return out
def spent() -> float:
return sum(json.load(open(f, encoding="utf-8"))["cost_usd"] for f in RAW.glob("ec-*.json"))
def cmd_plan():
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
calib = 0.612 # калибровка пробы 18 (cl100k → фактические prompt_tokens DeepSeek)
plan = plan_arms()
tin = sum(int(len(enc.encode("\n".join(m["content"] for m in msgs))) * calib)
for _, _, _, msgs, _ in plan)
n_diff = sum(1 for t, k, *_ in plan if k == "diff")
n_full = len(plan) - n_diff
print(f"вызовов: {len(plan)} ({n_diff} дифф + {n_full} full-regen), вход ≈{tin} ток. "
f"→ ${tin/1e6*P.PRICE_IN:.4f}")
# выход: дифф короткий (правки), full-regen длинный (весь текст) — берём наблюдённое в пробе 18
for lbl, d_out, f_out in (("ожидаемо", 1200, 2600), ("пессимистично", 4000, 8000)):
o = n_diff * d_out + n_full * f_out
print(f" {lbl:14s} выход {o} ток. → ИТОГО ${tin/1e6*P.PRICE_IN + o/1e6*P.PRICE_OUT:.4f}")
print(f" абсолютный потолок (все в cap 16000): "
f"${tin/1e6*P.PRICE_IN + len(plan)*16000/1e6*P.PRICE_OUT:.4f}")
print(f"ПОТОЛОК ПРОБЫ ${CEILING_USD}; живой стоп ${HARD_STOP_USD} + проекционный гард на вызов")
def cmd_run(only: list[str]):
plan = [p for p in plan_arms() if not only or any(p[0].startswith(f"ec-{o}") for o in only)]
cl = P.client()
total = spent()
if total:
print(f"уже потрачено этой пробой: ${total:.6f}")
worst_call = 16000 / 1e6 * P.PRICE_OUT + 2600 / 1e6 * P.PRICE_IN
for tag, kind, k, msgs, draft in plan:
if (RAW / f"{tag}.json").exists():
continue
if total + worst_call > CEILING_USD or total >= HARD_STOP_USD:
print(f"СТОП (проекционный гард): ${total:.4f} + худший вызов ${worst_call:.4f} "
f"> потолка ${CEILING_USD}")
return
rec = P.call(cl, msgs, tag)
total += rec["cost_usd"]
if kind == "diff":
r = apply_ops(draft, rec["content"])
(RAW / f"{tag}.applied.json").write_text(json.dumps(dict(
tag=tag, ops_total=r.ops_total, applied=r.applied, no_change=r.no_change,
malformed=r.malformed, rejected_notfound=r.rejected_notfound,
rejected_ambiguous=r.rejected_ambiguous, rejected_overlap=r.rejected_overlap,
rejected_empty=r.rejected_empty, details=r.details, text=r.text),
ensure_ascii=False, indent=1), encoding="utf-8")
print(f" apply: ops={r.ops_total} applied={r.applied} rejected={r.rejected} "
f"no_change={r.no_change} malformed={r.malformed}")
time.sleep(0.4)
print(f"TOTAL пробы Q4b: ${total:.6f}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--plan", action="store_true")
ap.add_argument("--run", nargs="*", default=None, help="D1 D2 F1 F5 (пусто = все)")
a = ap.parse_args()
if a.plan:
cmd_plan()
elif a.run is not None:
cmd_run(a.run)
else:
ap.print_help()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,46 @@
Ты — литературный редактор художественного перевода на русский язык. Тебе даны ИСХОДНЫЙ текст и его черновой перевод.
Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}».
Твой мандат — ТОЧЕЧНАЯ правка черновика со сверкой по исходнику. Ты НЕ переписываешь текст целиком: ты выдаёшь СПИСОК ОПЕРАЦИЙ ЗАМЕНЫ, каждая из которых чинит одно конкретное место.
Что править (в этом порядке важности):
- ВЕРНОСТЬ: искажения смысла против исходника (язык «{{source_lang}}») — перевёрнутая полярность, неверные числа и ранги, потерянные или добавленные смысловые атомы, неверно понятые реалии, кальки жестов и идиом (напр. поклон как «ударил головой»).
- ТЕРМИНЫ: имена собственные, термины и реалии — СТРОГО по приведённому глоссарию (если он приложен): приводи любые расхождения к каноническим формам, склоняя по контексту; не придумывай иных вариантов их передачи.
- СТИЛЬ: кальки с языка «{{source_lang}}», канцелярит, неестественные конструкции, битые словоформы. НО повтор, стоящий в тех же (параллельных) позициях исходника, — авторский рефрен: сохраняй его.
- Единицы и приёмы (общие для zh→ru): МЕРЫ приводи к привычным читателю единицам (时辰 = 2 часа, 三个时辰 ≈ шесть часов; доля 成 — десятые: 六成六 = 66%, а не «6,6»); СТИХИ и названия классики — по смыслу, не транслитерацией.
- Хонорифики: {{honorifics}}. Баланс форенизация/доместикация: {{venuti}}.
Чего НЕ делать:
- НЕ переверстывай абзацы, НЕ сливай и НЕ дели предложения ради структуры. Вёрстка — не твоя задача в этом проходе.
- НЕ «улучшай» места, которые верны и читаются нормально. **Пустой список правок — полноценный и уважаемый ответ.** Если черновик хорош, так и скажи.
- НЕ трогай то, чего не просили: каждая операция обязана чинить названный дефект.
ФОРМАТ ОТВЕТА (строго):
Если правок нет — выведи ровно одну строку:
NO_CHANGE
Иначе выведи одну или несколько операций, каждая ровно в такой форме:
<<<<<<< SEARCH
дословный фрагмент ЧЕРНОВИКА, который надо заменить
=======
текст замены
>>>>>>> REPLACE
Правила операций (нарушение = операция отбрасывается целиком):
1. Блок SEARCH должен ДОСЛОВНО совпадать с фрагментом черновика — копируй символ в символ, включая пунктуацию.
2. Фрагмент SEARCH должен быть УНИКАЛЕН в черновике. Если фраза встречается несколько раз — расширь фрагмент соседними словами, пока он не станет единственным.
3. Бери минимальный фрагмент, которого хватает для уникальности и осмысленной замены: слово, оборот, предложение. НЕ бери абзац целиком.
4. Никаких номеров строк, никаких «...» и сокращений внутри SEARCH.
5. Операции не должны пересекаться между собой.
6. Ничего, кроме операций (или NO_CHANGE), в ответе быть не должно — ни преамбул, ни комментариев, ни markdown-заголовков, ни объяснений.
---USER---
Исходный текст (язык «{{source_lang}}»):
{{text}}
Черновик перевода для редактуры:
{{draft}}