From 080c15d60a2cf83757e49d80192f59aaa09e7812 Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 4 Aug 2026 22:58:35 +0300 Subject: [PATCH] Freeze polygon pre-registration for the editor-wire probe and its harness before any paid call --- docs/experiments/18-editor-wire-probe.md | 212 +++++++++ eval/bank_arbitration/editor_wire_probe.py | 489 +++++++++++++++++++++ 2 files changed, 701 insertions(+) create mode 100644 docs/experiments/18-editor-wire-probe.md create mode 100644 eval/bank_arbitration/editor_wire_probe.py diff --git a/docs/experiments/18-editor-wire-probe.md b/docs/experiments/18-editor-wire-probe.md new file mode 100644 index 00000000..a0d04b85 --- /dev/null +++ b/docs/experiments/18-editor-wire-probe.md @@ -0,0 +1,212 @@ +# 18. Редакторский провод под доктриной закона (D39.104) + фронтир-пасс по голду + +**Полигон-сессия 04.08.2026.** Исполнение `docs/POLYGON_EDITOR_WIRE_PROBE_SESSION_PROMPT.md` +(оркестратор №13, санкции владельца D39.103/104). Зона: `eval/bank_arbitration/` + этот файл + пинг +в PROGRESS «Полигон». Сырьё durable `~/books/gu-zhenren/bank-arbitration/`, префикс `edp-*`. +Не закоммичено, кроме пре-рег-фриза (CLAUDE.md, единственное исключение полигона). +Потолки: проба A ≤ $0.30, проба C ≤ $0.50. + +--- + +## §0. ПРЕ-РЕГИСТРАЦИЯ (записано ДО первого платного вызова этой сессии; норма D39.46/D39.61) + +Дата фиксации: 2026-08-04, до любых платных вызовов. Всё ниже — фриз; расхождение исполнения с +этим текстом печатается как девиация с обоснованием. + +### 0.1 Провод пробы A (byte-faithful edit-стадии `pipeline-c1.yaml`) + +| Поле | Значение | Провенанс | +|---|---|---| +| модель | `deepseek-v4-pro` | `pipeline-c1.yaml` stages[edit].model (ИНТЕРИМ D39.22) | +| промпт | `backend/prompts/zh-ru/editor.md`, блок `---FEWSHOT---` ДРОПНУТ | stages[edit].few_shot: false | +| temperature | `0.4` (шлётся; thinking-режим её молча игнорирует — quirks 00) | stages[edit].temperature | +| reasoning | параметр **НЕ шлётся вовсе** ⇒ вендор-дефолт `high` | deepseek = `ReasoningNone` (`llm/capability.go:177-180`), stages[edit].reasoning:"off" ⇒ ключа на проводе нет; quirks 00 §3а | +| max_tokens | `16000` | `models.yaml:166` capabilities.min_max_tokens (флор v4-pro) | +| раскладка | `system(editor.md core)` → `system(банк-блок)` → `user(text+draft)` | `render.go` MessagesWithInjection | +| формат строк блока | `- src → «dst»` | `membank/memory.go:815` editorLines | + +**Байт-сверка провода — исполнением, ДО платных вызовов.** Overlay-тест (`go test -overlay`, файлы +живут в скрэтчпаде, в `backend/` не пишутся) прогнал РЕАЛЬНЫЕ `LoadPromptTemplate`+`SystemFor(false)`+ +`MessagesWithInjection` и `RenderEditorConstraintBlock` на окне w3 и напечатал sha256 каждого +сообщения. Совпадение с Python-харнессом побайтное: + +``` +Go WPMSG 0 role=system sha=cef8aba86410f996ab4624f6839f2acbf98a31ededa6a12a8070c03abb739645 +Py PYMSG 0 role=system sha=cef8aba86410f996ab4624f6839f2acbf98a31ededa6a12a8070c03abb739645 +Go WPMSG 1 role=system sha=775604eddb3346c0b566ff01d0affd7b222d00fd37684afa9104639ae83e107d +Py PYMSG 1 role=system sha=775604eddb3346c0b566ff01d0affd7b222d00fd37684afa9104639ae83e107d +Go WPMSG 2 role=user sha=547d516465c885890438b4e001c8d6d6a7c48641ce28701b6b6385bad66940fa +Py PYMSG 2 role=user sha=547d516465c885890438b4e001c8d6d6a7c48641ce28701b6b6385bad66940fa +Go WPBLOCK sha=775604eddb3346c0b566ff01d0affd7b222d00fd37684afa9104639ae83e107d (RenderEditorConstraintBlock, текущий editor_header) +``` + +Сверялся блок с ДЕЙСТВУЮЩИМ `editor_header`; кандидат-заголовок §B отличается от него ровно +вставкой оговорки (ниже дословно), строки блока не трогает. + +### 0.2 Окна и черновики + +Те же 6 окон источника, что `inject_probe.py` (`pick_windows()`, детерминирован, `coldrun-a` только +чтение): w0@para534 · w1@para709 · w2@para488 · w3@para372 · w4@para101 · w5@para274. +Черновики — **арм B того же сырья** (`inj-w{k}-B.json`, транслятор под ВЕРНЫМ законом, $0, уже +персистированы, все `finish=stop`): канон-конформный черновик = сильнейший тест вытеснения. + +### 0.3 Заголовки (кандидат-редакции §B промта; в движок НЕ вносятся) + +База — действующий `editor_header` (`backend/internal/lang/data/injection.txt` строка 7), дословно. + +**Единый закон-блок с оговоркой области** (армы A1, A2, AM-clause): +> КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого; глоссарий обязателен для употреблений термина КАК термина: игру слов, разбор знаков, буквальное прочтение передавай по смыслу пассажа, сохраняя узнаваемость канонической формы): + +**Закон БЕЗ оговорки** (арм AM-noclause) = действующий `editor_header` без единой правки: +> КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого): + +Секция `editor_unverified_header` НЕ рендерится (двухсекционка сворачивается в одну — §B промта); +маркер `⟨проверить⟩` на проводе НЕ рендерится (поведенчески пуст, research/24 §C). + +### 0.4 Армы (26 платных вызовов v4-pro + 1 транслятор-вызов на черновик AM) + +| Арм | Вызовов | Блок | Что мерит | +|---|---|---|---| +| **A0** | 6 | нет | изолированный контроль (его не было НИКОГДА) | +| **A1** | 6 | закон+оговорка, все строки ВЕРНЫЕ | исполнение закона на редакторе | +| **A2** | 6 | закон+оговорка, строка `元海 → «Юаньхай»` НЕВЕРНАЯ | цена неверного закона + межоконная консистентность | +| **AM** | 2 | закон+оговорка / закон без оговорки | работает ли оговорка области и не течёт ли | +| **A5** | 6 | нет | чинит ли редактор дефекты черновика | + +**Девиация от дизайна §C (обоснованная):** research/24 §C РОТИРОВАЛ неверный терм по окнам. Здесь +неверная строка ОДНА И ТА ЖЕ во всех 6 окнах — `元海`, единственный терм, присутствующий во всех +шести окнах (пересечение множеств термов = {元海}) и отрендеренный верной формой во всех шести +черновиках. Без этого метрика «судьба ОДНОЙ строки одинакова во всех 6 окнах» (§A промта) +неизмерима. Инъецируемая неверная форма — `Юаньхай` (реестр `TERMS` `inject_probe.py`). + +**AM — метаязыковое окно, НАТУРАЛЬНОЕ (не синтетика):** параграфы 517–521 источника — выбор +本命蛊 в шкафу гу. Несёт (а) `蛊名月光` — имя названо КАК ИМЯ («имя гу — Лунный свет»), при том что +канон-форма `月光蛊 → «Гу лунного света»`; (б) `弯弯如月` — буквальное «изогнут, как месяц»; +(в) обычные терминные употребления `月光蛊` в тех же 5 строках. Блок окна содержит ровно те термы +реестра, что встречаются в окне. +Черновик AM: 1 вызов транслятором проводом арма B (`deepseek-v4-flash`, `reasoning_effort:"low"`, +temp 0, ВЕРНЫЙ блок), тег `edp-AMdraft`. **Правило ветвления, фиксируется ЗДЕСЬ:** если черновик +передаёт `蛊名月光` ПО СМЫСЛУ (не приклеивает канон-форму к слову «имя») — идёт как есть; если +канонизировал — сажаю смысловой вариант «Имя этому гу — Лунный свет» и объявляю посадку в отчёте +(файл `edp-AMdraft-planted.txt`), результат помечается как посаженный. + +### 0.5 A5 — посаженные дефекты черновика (дословный список) + +По одному дефекту каждого класса в каждое из 6 окон, итого 6+6. Арм без банк-блока. +к1 = слово-абракадабра (несуществующее слово); к2 = смысловое искажение против ОРИГИНАЛА. + +| Окно | Класс | Было в черновике | Стало (посадка) | Против чего в исходнике | +|---|---|---|---|---| +| w0 | к1 | `также поразительна` | `также взгрумительна` | — | +| w0 | к2 | `они не выдержат такого непрерывного расхода` | `они легко выдержат такой непрерывный расход` | 再多的元石也**经不起**…消耗 | +| w1 | к1 | `сокровище природы` | `мреязное сокровище природы` | — | +| w1 | к2 | `на глазах неуклонно поднимался` | `на глазах неуклонно опускался` | 元海水位…不断**攀升** | +| w2 | к1 | `словно натянутая струна` | `словно натянутая струмель` | — | +| w2 | к2 | `Увы, тот так и не показался` | `Увы, тот в первую же ночь и показался` | 可惜,**就是不见**那酒虫 | +| w3 | к1 | `но при этом необычайно густой` | `но при этом необычайно гущавой` | — | +| w3 | к2 | `Фан Юань, не оборачиваясь, развернулся и пошёл обратно` | `Фан Юань обернулся и остался стоять на месте` | 方源直接**转身往回走** | +| w4 | к1 | `волна дерзновенной отваги` | `волна дерзновяжной отваги` | — | +| w4 | к2 | `была вполне приемлемой платой` | `была совершенно недопустимой утратой` | 春秋蝉的损失**完全可以接受** | +| w5 | к1 | `оттенок суровой удали` | `оттенок суровяглой удали` | — | +| w5 | к2 | `он наконец не смог идти дальше` | `он зашагал ещё быстрее` | 一直走到三十六步,终于**走不动了** | + +Скоринг A5: regex-предпроход («дефект ещё в выходе» / «смысл восстановлен») + **обязательная +глазная сверка каждой из 12 единиц; при расхождении авторитет — глаз**, regex печатается рядом. + +### 0.6 Скоринг A0/A1/A2 и развязка regex-конфаунда + +Метод — тот же regex-скорер, что `inject_probe.py --score` (нормализация NFKC · casefold · ё→е · +схлоп пробелов). **Найденный и развязанный конфаунд (проверка потребована промтом на классе +元海/真元):** голд-rx `真元` = `истинн\w* ци` матчится ВНУТРИ верной формы `元海` = «море истинной ци». +Развязка: перед поиском `真元` из нормализованного выхода вырезаются все вхождения голд-формы `元海`. +Ре-ран research/24 §C по персистированному сырью ОБОИМИ методами — числа §C не двигаются +(см. §A.0 ниже); конфаунд задевает 2 клетки (w1 армы C/D), и обе лежат в колонке НЕВЕРНОГО терма, +которая в знаменатель «верных строк» не входит. + +Знаменатели: «верные строки» = все термы блока кроме `元海` = 21 клетка (6+6+5+4+3+3 = 27 минус 6). + +### 0.7 Критерии (успех/провал, зафиксированы ДО запуска) + +1. **«Закон исполняется»** — A1 ≥ **5/6** окон, где ВСЕ верные строки окна пришли канонической + формой. Печатается также поклеточно (x/21) для сравнимости с §C. +2. **«Цена ошибки»** — A2: принятие неверной формы и вытеснение верной печатаются ЧИСЛОМ без + порога (это цена, не экзамен). +3. **«Консистентность держится»** — единогласие ≥ **5/6** однонаправленных решений по строке `元海` + в A2 (все 6 окон приняли ЛИБО все 6 отвергли). +4. **«Оговорка работает»** — метаязыковый пассаж передан ПО СМЫСЛУ при законе+оговорке И сломан + (канон-форма приклеена) при законе-без-оговорки. +5. **«Оговорка ТЕЧЁТ»** — ≥ **2/6** окон A1/A2 с отклонением от закона по ВЕРНЫМ строкам ⇒ + формулировку сузить и пере-гнать ТОТ арм, где leakage зафиксирован (в потолке). +6. **A5** — «X из Y» по каждому классу, порога НЕТ (первый замер класса). +7. Средние исходы (ровно 4/6 и т.п.) — «неопределённо на N=6», печатаются, не абсорбируются. + +### 0.8 Смета пробы A (напечатана ДО первого платного вызова) + +Калибровка токенизатора: cl100k → фактические `prompt_tokens` DeepSeek ×**0.612** (по 6 вызовам +сырья §C, разброс 0.597–0.622). Вход всех 26 вызовов ≈ **52 602** ток. → **$0.0229**. +Выход: нижняя оценка (только содержательный текст) 23 400 ток. → **$0.0432**; ожидаемая +104 000 ток. → **$0.1134**; абсолютный потолок (ВСЕ 26 вызовов упираются в 16 000) 416 000 ток. → +$0.3848. **Ожидаемая смета $0.11–0.14 < потолка $0.30.** Абсолютный потолок выше $0.30 достижим +только при 26/26 упоров в cap, поэтому в харнесс вшит **живой стоп $0.28**, проверяемый ПЕРЕД +каждым вызовом ⇒ потолок непробиваем механизмом, а не дисциплиной. Плюс 1 транслятор-вызов на +черновик AM ≈ $0.002. + +--- + +## §0C. ПРЕ-РЕГИСТРАЦИЯ ПРОБЫ C (фронтир-пасс по голду; потолок $0.50, «go» владельца D39.103 п.3) + +**Слаги — live `/models` 04.08.2026, не со слуха.** Владелец назвал «терра»/«луна»; в листинге +OpenAI (124 слага) присутствуют ровно `gpt-5.6-terra` и `gpt-5.6-luna` (плюс `gpt-5.6-sol`, которого +владелец НЕ называл — в пробу не берётся). Опознание однозначное, СТОП+релей не требуется. +DeepSeek-листинг: `deepseek-v4-flash`, `deepseek-v4-pro` — прежние два. + +**Цены — ТОЛЬКО прайс-страница вендора** `developers.openai.com/api/docs/pricing` (снято 04.08.2026), +за 1M токенов, короткий контекст / длинный контекст: + +| Слаг | in | cached in | out | (long) in | cached | out | +|---|---|---|---|---|---|---| +| `gpt-5.6-terra` | $2.00 | $0.20 | $12.00 | $4.00 | $0.40 | $18.00 | +| `gpt-5.6-luna` | $0.20 | $0.02 | $1.20 | $0.40 | $0.04 | $1.80 | +| `gpt-5.6-sol` (не в пробе) | $5.00 | $0.50 | $30.00 | $10.00 | $1.00 | $45.00 | +| `deepseek-v4-pro` | $0.435 | $0.003625 | $0.87 | — | — | — | + +(v4-pro — `models.yaml`, prices_checked 2026-07-10, подтверждено quirks 00 «цены пост-катовера не +изменились».) Батчи ростера ≈4.5k ток. ⇒ применяется КОРОТКИЙ контекст-тир. + +**Протокол** — тот же, что research/24 §B3: `consilium_probe.py`-ростер (45 голд-термов в банке + +18 экстра-поверхностей = 63 строки), 5 батчей, engine-faithful `RenderBatch`, боевой +`terminologist.md`, добавка CONF_NUM (как у пасса `glm1`, иначе несравнимо), temp 0 где применимо. +Чемпион для сравнения — `glm1` = **25/45** (пере-считано из сырья: ds1 20 · ds2 20 · ds3 21 · +dsw 18 · glm1 25 · mis1 23 · gro1 12). + +**Wire-квирк, который НЕ гадаю (правило двух направлений, quirks 00 шапка):** вендор-дока GPT-5.6 +описывает `reasoning.mode` + `reasoning.effort` (none…max) для **Responses API**; поддержка плоского +`reasoning_effort` в Chat Completions в доке не заявлена, а прецедент exp08 (xAI) — вложенная форма в +Chat Completions МОЛЧА ГЛОТАЕТСЯ. Поэтому ДО основных вызовов идёт **микро-wire-проба** (≈$0.002): +на каждом слаге по 2 крошечных вызова с эффортами `none` и `high`; ручка считается ЖИВОЙ, только если +`reasoning_tokens` между ними различаются. Транспорт основного прогона выбирается по её результату +(Chat Completions при живой ручке; иначе Responses API), выбор печатается в отчёт. +Прочее по квиркам: `max_completion_tokens` вместо `max_tokens`, `temperature` не слать. +Эффорт основного прогона: **low** для terra/luna (дешёвый тир, зеркало ds1); для `deepseek-v4-pro` +reasoning-параметр НЕ шлётся (`low` на pro маппится в `high` — ручки бюджета у pro нет, quirks 00 §3а). + +**Смета C:** вход ≈19k ток. на пасс. v4-pro: $0.008 вход + выход (высокий эффорт, оценка 30k ток.) +$0.026 ≈ **$0.034**. luna: $0.004 + $0.048 ≈ **$0.05**. terra: $0.038 + (оценка 8k ток.) $0.096 ≈ +**$0.13**. Итого ожидаемо **≈$0.22** при потолке $0.50; живой стоп харнесса **$0.45**. + +**Критерий (фриз):** «фронтир оправдан для роли» — парное преимущество кандидата над `glm1` +(термы «кандидат прав ∧ glm1 неправ» минус обратные) ≥ **+5** на 45 голд-термах **И** названа +цена/книга по методике research/24 §B5. Меньше +5 — «не оправдан» (легитимный исход). + +**Data-sharing** на ключе OpenAI отключён — слово владельца, D39.103 п.3 (со слов промта +оркестратора; сессия это не верифицировала). + +--- + +## §A. Проба A — результаты + +*(заполняется после прогона)* + +## §C. Проба C — результаты + +*(заполняется после прогона)* diff --git a/eval/bank_arbitration/editor_wire_probe.py b/eval/bank_arbitration/editor_wire_probe.py new file mode 100644 index 00000000..3dd54ff5 --- /dev/null +++ b/eval/bank_arbitration/editor_wire_probe.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python3 +"""Проба A (промт POLYGON_EDITOR_WIRE_PROBE §A): РЕДАКТОРСКИЙ провод под доктриной закона D39.104. + +Отличие от inject_probe.py (тот мерил ТРАНСЛЯТОР-провод): здесь роль — РЕДАКТОР, вход +байт-близко к edit-стадии шиппинг-конфига c1: + messages = [system(editor.md CORE, few_shot ДРОПНУТ), system(банк-блок), user(text+draft)] + model=deepseek-v4-pro · temperature=0.4 · max_tokens=16000 · reasoning_effort НЕ СЛАТЬ. +Формат строк блока — как editorLines (membank/memory.go:798-829): "- src → «dst»". + +Армы: A0 (без блока) · A1 (единый закон-блок, ВЕРНАЯ строка) · A2 (тот же блок, НЕВЕРНАЯ +строка 元海→«Юаньхай» во ВСЕХ 6 окнах — межоконная консистентность) · AM (метаязыковое окно, +закон+оговорка против закона-без-оговорки) · A5 (посаженные дефекты черновика, без блока). + +Черновики — арм B транслятор-сырья research/24 §C ($0, уже персистированы). +Сырьё durable ~/books/gu-zhenren/bank-arbitration/, префикс edp-*. +""" +from __future__ import annotations +import argparse +import json +import os +import re +import sys +import time +import unicodedata +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" +SRC = Path.home() / "books" / "gu-zhenren" / "coldrun-a" / "guzhenren-ch1-10.gb18030.txt" +load_dotenv(REPO / "eval" / ".env") + +sys.path.insert(0, str(HERE)) +from inject_probe import TERMS, pick_windows, norm # noqa: E402 (тот же реестр термов и те же окна) + +# --- Провод: ровно edit-стадия pipeline-c1.yaml ------------------------------------------------- +MODEL = "deepseek-v4-pro" +TEMPERATURE = 0.4 # pipeline-c1.yaml stages[edit].temperature +MAX_TOKENS = 16000 # models.yaml:166 capabilities.min_max_tokens (флор v4-pro) +FEW_SHOT = False # pipeline-c1.yaml stages[edit].few_shot: false → блок ---FEWSHOT--- дропается +# reasoning_effort НЕ шлём вовсе: deepseek = ReasoningNone (llm/capability.go:177-180), +# reasoning:"off" в конфиге ⇒ ключа на проводе НЕТ ⇒ вендор-дефолт high (quirks 00 §3а). +PRICE_IN, PRICE_CACHED, PRICE_OUT = 0.435, 0.003625, 0.87 # models.yaml deepseek-v4-pro +HARD_STOP_USD = 0.28 # пре-рег: живой стоп ниже потолка $0.30 + +EDITOR = REPO / "backend/prompts/zh-ru/editor.md" +TRANSLATOR = REPO / "backend/prompts/zh-ru/translator.md" +USER_SEP = "\n---USER---\n" +FEWSHOT_SEP = "\n---FEWSHOT---\n" + +# --- §B: кандидат-редакции заголовков (в движок НЕ вносятся) ------------------------------------ +# База — действующий editor_header (backend/internal/lang/data/injection.txt строка 7), дословно. +EDITOR_HEADER_BASE = ( + "КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть " + "передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по " + "контексту; не вводи иных вариантов и не меняй ничего другого)" +) +CLAUSE = ( + "; глоссарий обязателен для употреблений термина КАК термина: игру слов, разбор знаков, " + "буквальное прочтение передавай по смыслу пассажа, сохраняя узнаваемость канонической формы" +) +HEADER_LAW_CLAUSE = EDITOR_HEADER_BASE + CLAUSE + ":" # единый закон-блок §B (A1/A2/AM-clause) +HEADER_LAW_PLAIN = EDITOR_HEADER_BASE + ":" # закон без оговорки (AM-noclause) + +WRONG_TERM = "元海" # единственный терм, присутствующий во ВСЕХ 6 окнах → метрика консистентности +AM_PARAS = (517, 522) # метаязыковое окно: 蛊名月光 (имя как имя) + 弯弯如月 (буквальная луна) + +RENDER_VARS = {"source_lang": "zh", "target_lang": "ru", "genre": "вебновелла", + "audience": "взрослые читатели вебновелл", "title": "蛊真人", "venuti": "0.60", + "honorifics": "keep", "transcription": "palladius", "footnotes": "minimal"} + + +def strip_comments(s: str) -> str: + """Зеркало render.go:99-116 (stripPromptComments): SHA берётся с канонической формы.""" + out, rest = [], s + while True: + i = rest.find("") + if j < 0: + raise ValueError("unterminated comment") + rest = rest[j + 3:] + + +def load_template(path: Path) -> tuple[str, str, str]: + """Зеркало render.go LoadPromptTemplate: (core system, fewshot, user).""" + canon = strip_comments(path.read_text(encoding="utf-8")) + sys_part, user = canon.split(USER_SEP, 1) + head = sys_part.split(FEWSHOT_SEP, 1) + core = head[0].strip() + few = head[1].strip() if len(head) == 2 else "" + return core, few, user.strip() + + +def render(tpl: str, text: str, draft: str) -> str: + """Зеркало render.go Render: ОДИН проход, значения не пере-сканируются.""" + vals = dict(RENDER_VARS, text=text, draft=draft) + out, rest = [], tpl + while True: + i = rest.find("{{") + if i < 0: + out.append(rest) + return "".join(out) + out.append(rest[:i]) + rest = rest[i:] + end = rest.find("}}") + if end < 0: + raise ValueError("unterminated placeholder") + name = rest[2:end] + if name not in vals: + raise ValueError(f"unknown placeholder {{{{{name}}}}}") + out.append(vals[name]) + rest = rest[end + 2:] + + +def editor_messages(text: str, draft: str, block: str | None) -> list[dict]: + core, few, user_tpl = load_template(EDITOR) + system = core + ("\n\n" + few if FEW_SHOT and few else "") # SystemFor(fewShotOn) + msgs = [{"role": "system", "content": render(system, text, draft)}] + if block and block.strip(): + msgs.append({"role": "system", "content": block}) + msgs.append({"role": "user", "content": render(user_tpl, text, draft)}) + return msgs + + +def editor_block(terms: list[str], header: str, wrong: str | None) -> str: + """Формат editorLines (memory.go:815): '- ' + src + ' → «' + dst + '»'.""" + lines = [] + for t in terms: + gold, bad, _, _ = TERMS[t] + dst = bad if t == wrong else gold + lines.append(f"- {t} → «{dst}»") + return header + "\n" + "\n".join(lines) + + +# --- A5: посаженные дефекты черновика (дословный список — во фризе пре-рега) --------------------- +# (класс, что заменить в черновике, на что, regex «дефект ещё в выходе», regex «починено по смыслу») +DEFECTS = { + 0: [("k1", "также поразительна", "также взгрумительна", r"взгрумительн", r"поразительн|порази|впечатл|изумител|поразит"), + ("k2", "они не выдержат такого непрерывного расхода", + "они легко выдержат такой непрерывный расход", + r"легко выдерж", r"не выдерж|не выстоят|не хват|иссяк|не выносят")], + 1: [("k1", "сокровище природы", "мреязное сокровище природы", r"мреязн", r"сокровищ"), + ("k2", "на глазах неуклонно поднимался", "на глазах неуклонно опускался", + r"неуклонно опуска|неуклонно понижа|неуклонно снижа", r"поднима|повыша|нараста|расту|рос\b|привстава|прибыва|подъём|увеличива")], + 2: [("k1", "словно натянутая струна", "словно натянутая струмель", r"струмел", r"струн"), + ("k2", "Увы, тот так и не показался", "Увы, тот в первую же ночь и показался", + r"в первую же ночь и показа", r"так и не показа|не появ|не показыва|не давал себя|не показал")], + 3: [("k1", "но при этом необычайно густой", "но при этом необычайно гущавой", r"гущав", r"густ|вязк|плотн"), + ("k2", "Фан Юань, не оборачиваясь, развернулся и пошёл обратно", + "Фан Юань обернулся и остался стоять на месте", + r"остал\w{0,3} стоять на месте", r"развернул|поверн\w+ (назад|обратно)|пошёл обратно|двинул\w* обратно|побрёл обратно|пошел обратно")], + 4: [("k1", "волна дерзновенной отваги", "волна дерзновяжной отваги", r"дерзновяжн", r"дерзнов|отваг|воодушевл|решимост"), + ("k2", "была вполне приемлемой платой", "была совершенно недопустимой утратой", + r"недопустим\w* утрат", r"приемлем|допустим|не столь велик|стоила того|можно смириться|терпим")], + 5: [("k1", "оттенок суровой удали", "оттенок суровяглой удали", r"суровягл", r"суров|удал|лих|дерзк"), + ("k2", "он наконец не смог идти дальше", "он зашагал ещё быстрее", + r"зашагал ещ[её] быстрее", r"не смог идти|не мог идти|остановил|не в силах|дальше не|встал\b|замер")], +} + + +def planted_draft(k: int, draft: str) -> tuple[str, list]: + out = draft + for cls, old, new, bad_rx, fix_rx in DEFECTS[k]: + if old not in out: + raise SystemExit(f"w{k}: посадка не нашла якорь {old!r} в черновике") + out = out.replace(old, new, 1) + return out, DEFECTS[k] + + +# --- окна --------------------------------------------------------------------------------------- +def draft_of(k: int) -> str: + return json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))["content"] + + +def am_window() -> tuple[str, list[str]]: + paras = [p for p in SRC.read_text(encoding="gb18030").split("\n") if p.strip()] + buf = "\n".join(paras[AM_PARAS[0]:AM_PARAS[1]]) + "\n" + terms = [t for t in TERMS if t in buf] + return buf, terms + + +# --- вызовы ------------------------------------------------------------------------------------- +def client() -> OpenAI: + return OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", timeout=900) + + +def call(cl, msgs, tag, model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, + extra=None, prices=(PRICE_IN, PRICE_CACHED, PRICE_OUT)) -> dict: + kw = dict(model=model, messages=msgs, max_tokens=max_tokens, temperature=temperature) + if extra: + kw["extra_body"] = extra + t0 = time.time() + r = cl.chat.completions.create(**kw) + ch = r.choices[0] + u = r.usage + pt, ct = u.prompt_tokens or 0, u.completion_tokens or 0 + cached = getattr(getattr(u, "prompt_tokens_details", None), "cached_tokens", 0) or 0 + pin, pcache, pout = prices + cost = (pt - cached) / 1e6 * pin + cached / 1e6 * pcache + ct / 1e6 * pout + rec = dict(tag=tag, model=model, model_returned=r.model, finish=ch.finish_reason, + ts=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + temperature=temperature, max_tokens=max_tokens, extra_body=extra or {}, + prompt_tokens=pt, cached_tokens=cached, completion_tokens=ct, + reasoning_chars=len(getattr(ch.message, "reasoning_content", None) or ""), + cost_usd=round(cost, 6), latency_s=round(time.time() - t0, 1), + messages=msgs, content=ch.message.content or "") + (RAW / f"{tag}.json").write_text(json.dumps(rec, ensure_ascii=False, indent=1), encoding="utf-8") + print(f"[{tag}] finish={ch.finish_reason} in={pt}(c{cached}) out={ct} " + f"${cost:.6f} {rec['latency_s']}s content={len(rec['content'])}c") + return rec + + +def slug_check() -> None: + cl = client() + ids = [m.id for m in cl.models.list().data] + print(f"/models deepseek: {ids}") + if MODEL not in ids: + raise SystemExit(f"слаг {MODEL} НЕ листится — СТОП") + + +def build_arms(am_draft: str | None): + """Полный план вызовов: [(tag, messages, note)].""" + wins = pick_windows() + plan = [] + for k, (i, buf, terms) in enumerate(wins): + d = draft_of(k) + plan.append((f"edp-A0-w{k}", editor_messages(buf, d, None), "без блока")) + for k, (i, buf, terms) in enumerate(wins): + d = draft_of(k) + plan.append((f"edp-A1-w{k}", editor_messages(buf, d, editor_block(terms, HEADER_LAW_CLAUSE, None)), + "закон+оговорка, ВЕРНАЯ")) + for k, (i, buf, terms) in enumerate(wins): + d = draft_of(k) + plan.append((f"edp-A2-w{k}", editor_messages(buf, d, editor_block(terms, HEADER_LAW_CLAUSE, WRONG_TERM)), + f"закон+оговорка, НЕВЕРНАЯ {WRONG_TERM}")) + if am_draft is not None: + buf, terms = am_window() + plan.append(("edp-AM-clause", editor_messages(buf, am_draft, editor_block(terms, HEADER_LAW_CLAUSE, None)), + "метаязык: закон+оговорка")) + plan.append(("edp-AM-noclause", editor_messages(buf, am_draft, editor_block(terms, HEADER_LAW_PLAIN, None)), + "метаязык: закон БЕЗ оговорки")) + for k, (i, buf, terms) in enumerate(wins): + d, _ = planted_draft(k, draft_of(k)) + plan.append((f"edp-A5-w{k}", editor_messages(buf, d, None), "посаженные дефекты, без блока")) + return plan + + +def cmd_plan(): + """Смета ДО первого платного вызова. Токены — калибровка cl100k против фактических + prompt_tokens сырья §C (печатается), выход — полоса от содержательного до потолка.""" + import tiktoken + enc = tiktoken.get_encoding("cl100k_base") + + # --- калибровка на фактическом сырье §C (транслятор-армы, prompt_tokens известны) --- + core_t, few_t, user_t = load_template(TRANSLATOR) + ratios = [] + for k, (i, buf, terms) in enumerate(pick_windows()): + rec = json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8")) + s = render(core_t, buf, "") + "\n" + rec["injection"] + "\n" + rec["user"] + ratios.append(rec["prompt_tokens"] / len(enc.encode(s))) + calib = sum(ratios) / len(ratios) + print(f"калибровка cl100k→deepseek prompt_tokens: ×{calib:.3f} (по 6 вызовам §C, разброс " + f"{min(ratios):.3f}..{max(ratios):.3f})") + + plan = build_arms(am_draft="ЗАГЛУШКА " * 200) + tot_in = 0 + for tag, msgs, note in plan: + n = int(len(enc.encode("\n".join(m["content"] for m in msgs))) * calib) + tot_in += n + print(f" {tag:18s} in≈{n:5d} ток. — {note}") + # выход: нижняя оценка — только содержательный текст (≈ длина черновика), верхняя — потолок + lo_out = 26 * 900 + mid_out = 26 * 4000 + hi_out = 26 * MAX_TOKENS + def money(o): + return tot_in / 1e6 * PRICE_IN + o / 1e6 * PRICE_OUT + print(f"\nвсего вход ≈{tot_in} ток. → ${tot_in/1e6*PRICE_IN:.4f}") + print(f"выход: нижняя {lo_out} ток. ${money(lo_out):.4f} · ожидаемая {mid_out} ток. " + f"${money(mid_out):.4f} · АБСОЛЮТНЫЙ ПОТОЛОК {hi_out} ток. ${money(hi_out):.4f}") + print(f"ПОТОЛОК ПРОБЫ $0.30; живой стоп харнесса ${HARD_STOP_USD} (проверяется ПЕРЕД каждым вызовом)") + print(f"+ 1 транслятор-вызов (черновик AM-окна, deepseek-v4-flash low) ≈ $0.002") + + +def cmd_amdraft(): + """Черновик метаязыкового окна тем же проводом, что арм B §C (flash, low, temp 0, верный блок).""" + from inject_probe import render_translator, block_for + buf, terms = am_window() + system, user = render_translator(buf) + inj = block_for(terms, wrong=None, marked=False) + cl = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", timeout=600) + msgs = [{"role": "system", "content": system}, {"role": "system", "content": inj}, + {"role": "user", "content": user}] + call(cl, msgs, "edp-AMdraft", model="deepseek-v4-flash", temperature=0, max_tokens=16000, + extra={"reasoning_effort": "low"}, prices=(0.14, 0.0028, 0.28)) + + +def cmd_run(only: list[str]): + am = None + f = RAW / "edp-AMdraft.json" + if f.exists(): + am = json.load(open(f, encoding="utf-8"))["content"] + planted = RAW / "edp-AMdraft-planted.txt" + if planted.exists(): # пре-рег ветка «черновик канонизировал» → посадка объявлена + am = planted.read_text(encoding="utf-8") + plan = [p for p in build_arms(am) if not only or any(p[0].startswith(f"edp-{o}") for o in only)] + cl = client() + total = 0.0 + for tag, msgs, note in plan: + if (RAW / f"{tag}.json").exists(): + print(f"[{tag}] уже персистирован — пропуск") + continue + if total >= HARD_STOP_USD: + print(f"СТОП: накоплено ${total:.4f} ≥ ${HARD_STOP_USD}") + break + rec = call(cl, msgs, tag) + total += rec["cost_usd"] + time.sleep(0.5) + print(f"TOTAL этого запуска: ${total:.6f}") + + +# --- скоринг ------------------------------------------------------------------------------------ +def hits(out_norm: str, term: str) -> tuple[bool, bool]: + """gold/wrong по regex харнесса. ⚠ РАЗВЯЗКА КОНФАУНДА (оговорка приёмки research/24 (а)): + голд-rx 真元 (r'истинн\\w* ци') матчится ВНУТРИ верного перевода 元海 «море истинной ци». + Перед поиском 真元 вырезаем все вхождения голд-формы 元海.""" + gold, bad, grx, brx = TERMS[term] + hay = out_norm + if term == "真元": + hay = re.sub(TERMS["元海"][2], " ", hay) + return bool(re.search(grx, hay)), bool(re.search(brx, out_norm)) + + +def out_of(tag: str) -> str | None: + f = RAW / f"{tag}.json" + if not f.exists(): + return None + return norm(json.load(open(f, encoding="utf-8"))["content"]) + + +def cmd_score(): + wins = pick_windows() + print("=== A0/A1/A2: следование закону по ВЕРНЫМ строкам блока (терм ≠ 元海) ===") + per_arm = {} + for arm in ("A0", "A1", "A2"): + g = n = 0 + detail = [] + for k, (i, buf, terms) in enumerate(wins): + o = out_of(f"edp-{arm}-w{k}") + if o is None: + continue + for t in terms: + if t == WRONG_TERM: + continue + gh, wh = hits(o, t) + g += gh + n += 1 + detail.append((k, t, gh, wh)) + per_arm[arm] = (g, n, detail) + print(f" арм {arm}: {g}/{n} канонических форм в выходе") + print("\n=== строка 元海 (в A2 инъецирована НЕВЕРНОЙ «Юаньхай») ===") + for arm in ("A0", "A1", "A2"): + rows = [] + for k, (i, buf, terms) in enumerate(wins): + o = out_of(f"edp-{arm}-w{k}") + if o is None: + continue + gh, wh = hits(o, WRONG_TERM) + rows.append((k, gh, wh)) + gold_n = sum(1 for _, gh, _ in rows if gh) + wrong_n = sum(1 for _, _, wh in rows if wh) + print(f" арм {arm}: gold {gold_n}/{len(rows)} · wrong {wrong_n}/{len(rows)} " + + " ".join(f"w{k}:(g={int(gh)},w={int(wh)})" for k, gh, wh in rows)) + print("\n консистентность A2 (одна строка, 6 окон): " + + describe_consistency([(k,) + hits(out_of(f"edp-A2-w{k}"), WRONG_TERM) + for k in range(6) if out_of(f"edp-A2-w{k}") is not None])) + print("\n=== A5: фикс-рейт посаженных дефектов (арм без блока) ===") + for cls in ("k1", "k2"): + fixed = tot = 0 + rows = [] + for k in range(6): + o = out_of(f"edp-A5-w{k}") + if o is None: + continue + for c, old, new, bad_rx, fix_rx in DEFECTS[k]: + if c != cls: + continue + still = bool(re.search(bad_rx, o)) + restored = bool(re.search(fix_rx, o)) + tot += 1 + fixed += (not still) + rows.append(f"w{k}:{'FIX' if not still else 'ЖИВ'}{'/восст' if restored else ''}") + print(f" класс {cls}: исправлено {fixed}/{tot} " + " ".join(rows)) + print("\n=== AM: метаязыковое окно ===") + for tag in ("edp-AM-clause", "edp-AM-noclause"): + f = RAW / f"{tag}.json" + if not f.exists(): + continue + c = json.load(open(f, encoding="utf-8"))["content"] + print(f"--- {tag} ---\n{c}\n") + + +def describe_consistency(rows) -> str: + if not rows: + return "нет данных" + accepted = sum(1 for _, gh, wh in rows if wh) + displaced = sum(1 for _, gh, wh in rows if wh and not gh) + return (f"неверная форма принята в {accepted}/{len(rows)} окнах, из них полностью вытеснила " + f"верную в {displaced}/{len(rows)}; единогласие " + f"{max(accepted, len(rows) - accepted)}/{len(rows)}") + + +def cmd_selfcheck(): + """Ре-ран §C по персистированному сырью: (1) воспроизводим таблицу research/24 §C + ТЕМ ЖЕ методом, (2) показываем эффект развязки конфаунда 真元/元海.""" + wins = pick_windows() + from inject_probe import WRONG_PRIORITY + for fix in (False, True): + print(f"--- конфаунд {'РАЗВЯЗАН' if fix else 'как в §C'} ---") + for arm in "ABCD": + g = n = 0 + wg = wn = 0 + for k, (i, buf, terms) in enumerate(wins): + cand = [t for t in WRONG_PRIORITY if t in terms] + wrong = cand[k % len(cand)] + f = RAW / f"inj-w{k}-{arm}.json" + if not f.exists(): + continue + o = norm(json.load(open(f, encoding="utf-8"))["content"]) + for t in terms: + gold, bad, grx, brx = TERMS[t] + hay = o + if fix and t == "真元": + hay = re.sub(TERMS["元海"][2], " ", hay) + gh, wh = bool(re.search(grx, hay)), bool(re.search(brx, o)) + if t == wrong: + wn += 1 + wg += wh + else: + n += 1 + g += gh + print(f" арм {arm}: верные строки {g}/{n} · неверная принята {wg}/{wn}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--plan", action="store_true", help="смета, без вызовов") + ap.add_argument("--slugcheck", action="store_true") + ap.add_argument("--amdraft", action="store_true", help="1 транслятор-вызов: черновик AM-окна") + ap.add_argument("--run", nargs="*", default=None, help="A0 A1 A2 AM A5 (пусто = все)") + ap.add_argument("--score", action="store_true") + ap.add_argument("--selfcheck", action="store_true", help="ре-ран §C по сырью, $0") + ap.add_argument("--dump", help="напечатать messages одного тега (байт-сверка провода)") + a = ap.parse_args() + if a.plan: + cmd_plan() + elif a.slugcheck: + slug_check() + elif a.amdraft: + cmd_amdraft() + elif a.run is not None: + cmd_run(a.run) + elif a.score: + cmd_score() + elif a.selfcheck: + cmd_selfcheck() + elif a.dump: + am = None + f = RAW / "edp-AMdraft.json" + if f.exists(): + am = json.load(open(f, encoding="utf-8"))["content"] + for tag, msgs, note in build_arms(am or "ЧЕРНОВИК-ЗАГЛУШКА"): + if tag == a.dump: + for m in msgs: + print(f"----- role={m['role']} ({len(m['content'])} симв) -----") + print(m["content"]) + else: + ap.print_help() + + +if __name__ == "__main__": + main()