From 09ca24b50ad3a69bad4d2adc326233ca82d8cca9 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sun, 19 Jul 2026 04:03:39 +0300 Subject: [PATCH] Land the ratified implementation plan as D39.12 with design11 verifications, record the mistral rate-limit quirk, and issue the backend single-pack session prompt --- docs/BACKEND_PLAN11_SESSION_PROMPT.md | 179 +++ docs/PROGRESS.md | 6 +- docs/README.md | 4 +- docs/architecture/05-decisions-log.md | 12 +- docs/architecture/11-implementation-plan.md | 1069 +++++++++++++++++ ...GN_SYNTHESIS_SESSION_PROMPT_2026-07-19.md} | 0 docs/experiments/00-provider-quirks.md | 1 + eval/design11/ws1_wave_cogs.py | 239 ++++ eval/design11/ws2_chunk_sim.py | 176 +++ eval/design11/ws2_fertility_verify.py | 132 ++ eval/design11/ws3_miner_verify.py | 91 ++ eval/design11/ws3_seeddelta.py | 173 +++ eval/design11/ws4_banknote_verify.py | 119 ++ eval/design11/ws5_checkers_verify.py | 187 +++ 14 files changed, 2384 insertions(+), 4 deletions(-) create mode 100644 docs/BACKEND_PLAN11_SESSION_PROMPT.md create mode 100644 docs/architecture/11-implementation-plan.md rename docs/{DESIGN_SYNTHESIS_SESSION_PROMPT.md => archive/prompts/DESIGN_SYNTHESIS_SESSION_PROMPT_2026-07-19.md} (100%) create mode 100644 eval/design11/ws1_wave_cogs.py create mode 100644 eval/design11/ws2_chunk_sim.py create mode 100644 eval/design11/ws2_fertility_verify.py create mode 100644 eval/design11/ws3_miner_verify.py create mode 100644 eval/design11/ws3_seeddelta.py create mode 100644 eval/design11/ws4_banknote_verify.py create mode 100644 eval/design11/ws5_checkers_verify.py diff --git a/docs/BACKEND_PLAN11_SESSION_PROMPT.md b/docs/BACKEND_PLAN11_SESSION_PROMPT.md new file mode 100644 index 0000000..badf0d8 --- /dev/null +++ b/docs/BACKEND_PLAN11_SESSION_PROMPT.md @@ -0,0 +1,179 @@ +# Промт: сессия БЭКЕНД — единый пак по плану 11 (волновой раннер · чанкер/единицы · банк W1.5 · банкнота · чекеры · армы), 2026-07-19 + +> **Место в очереди (пост-D39.12):** исследовательская программа арх-ресета (D39–D39.10) и дизайн-синтез +> (D39.11) ЗАВЕРШЕНЫ. План **`docs/architecture/11-implementation-plan.md` РАТИФИЦИРОВАН (D39.12)** после трёх +> рубежей верификации ($0-симуляции с числами — исполнены и воспроизводимы). Эта сессия — СТРОЙКА строго по +> нему. **Решение владельца: весь объём — ОДНОЙ сессией**, четырьмя блоками с чекпойнт-ревью между. + +## Кто ты и что сдаёшь + +Бэкенд-сессия TextMachine. **Зона записи: `backend/`** (вкл. `configs/`, `prompts/`); всё остальное читать +можно, править нельзя (расхождения — пингом через отчёт/владельца). **НЕ коммитить** — лендит оркестратор +после приёмки (git-гардрейл CLAUDE.md: никаких reset --hard/rebase/checkout поверх грязного дерева). Сдаёшь: +1. Код WS1–WS6 по плану + тесты из (д)-секций (пины/golden/race/kill-9). +2. **`backend/PACK11_REPORT.md`** — per-буллет чек-лист (✓/девиация+причина), тест-логи, golden-диффы, + раздел «багфиксы вне плана», раздел «структурные переносы» (если рефакторил), residual. + +## Онбординг (порядок чтения, ~45 мин) + +1. `CLAUDE.md` (гардрейлы: `.env` НЕ читать · 18+ уровень 3 · PUML не рендерить · книга и производные ВНЕ git) + → `backend/README.md` (**инварианты 1–8 — ломать нельзя, каждый закреплён тестами**) → + `docs/architecture/03-implementation-notes.md`. +2. **`docs/architecture/11-implementation-plan.md` ЦЕЛИКОМ — это твоя спека.** Структура: WS1–WS6, каждый + (а) что менять / (б) алгоритм / (в) конфиг+снапшот / (г) исполненная $0-верификация с числами / + (д) тест-спека ПОСЛЕ стройки / (е) открытое; §7 порядок · §8 манифест resnapshot · §9 диспозиции · + §10 развилки владельца · §11 гейтнутые армы. План верифицирован трижды — **не пере-решай его решения**; + несогласие или найденная дыра = эскалация запиской, не тихая девиация. +3. `docs/architecture/05-decisions-log.md`: карта актуальности в шапке + D39–D39.12 подробно; + `09-target-architecture.md` §0.1 (**инвариант общности — НЕСУЩИЙ:** никакой захардкоженной структуры + книги/пары; пара = ось данных). +4. **`docs/experiments/00-provider-quirks.md`** — ПЕРЕД любым кодом около адаптеров/конфигов моделей + (вкл. свежую запись «mistral rate-limiter» в §Транспорт). +5. Референсы по мере работы: `eval/exp16/*.py` (**frozen miner-v1 — эталон порта WS3**, SHA в exp16 §1.10), + `eval/design11/` (симуляции/фикстуры плана — твои golden-входы; python — `eval/.venv`), `research/20` §B3 + (банкнота), `research/19` §B (контекст чанкера). Авто-память сессий — point-in-time, верь коду/докам. + +## Порядок работы (по §7 плана; единый пак, 4 блока, гейты между) + +- **Блок A** = WS1 (волновой раннер) + WS2 (чанкер-бюджет + edit-единицы) — несущий каркас, разблокирует всё. +- **Блок B** = WS3 (Go-майнер W1.5) + WS4 (банкнота, 12 точек). +- **Блок C** = WS5 (DC-чекеры + omission-бэкстоп + данные пакета zh-ru + `tmctl export --pairs`). +- **Блок D** = WS6 (армы конфигом + связка с rate-guard + атрибуция) — конфиг-пак. + +После КАЖДОГО из блоков A/B/C — чекпойнт-ревью (см. §Верификация). Единый `--resnapshot` по §8 в сессии +**НЕ исполнять** (плановое событие после приёмки, одна переоплата); но **golden re-capture** под +ратифицированную смену поведения — исполнять по мере: маскированный структурный дифф обязан быть объясним +(только хеши/версии/новые поля) и прикладывается в отчёт каждый раз. + +## Чек-лист исполнения (буллеты; ТОЧНАЯ спека — в плане; при расхождении буллета с планом побеждает ПЛАН) + +**WS1 (план §1):** +- Eager-build ВСЕХ клиентов в W0 (`Stages[].Model ∪ EscalateTo`); `client()` в волнах read-only + громкий + отказ на miss (закрывает гонку `runner.go:171-181`; трипваер на будущую 3-ю модель-ось). +- W0-precompute sticky-цепочки (`Select` — чистая src-производная функция). Golden: инъекц-байты precompute ≡ + байтам последовательного раннера над НОВЫМ чанк-сетом WS2, fresh↔resume. +- **Пер-волновой снапшот:** `snapshot_W1` (draft-стадии + base-bank = хеш approved `Source∈{seed,ruby}`), + `snapshot_W2` (edit-стадии + enriched-bank = все approved вкл. mined). Механика для ОБОИХ состояний + `PostcheckGate` — по §1(в). +- **Инвариант секвенирования (НЕСУЩИЙ):** W2 НИКОГДА не пере-рендерит/пере-хеширует draft-стадию — редактор + читает черновик по `final_hash`-чекпоинту. Пин: draft `request_hash` байт-стабилен под обогащённым банком. +- W1.5-стоп-механика: стоп после W1 → карта подписи → подпись владельца → **персист дельты ОТДЕЛЬНЫМ + mined-write-путём (штамп `Source:mined`; НЕ через `loadGlossarySeed` — тот хардкодит `Source:seed`, + `memseed.go:131`)** → resume в W2. Подпись mid-run трогает ТОЛЬКО mined-строки; правка seed/ruby-строки + после W1 = громкий resnapshot-стоп, не тихий re-bill (пин-тест). +- Деньги под ∥: Reserve/settle из N горутин через single-writer; kill-9-тест с N живыми резервами + (`committed==SUM`, каждый потерянный вызов пере-оплачен один раз); overshoot-тест `cost>estimate`. +- Scheduler-aware ценовые окна (конфиг провайдера, не хардкод; DeepSeek ×2 = риск-пин до ре-чека 24.07) + + **пер-модельный rate-guard**: семафор конкуренции `models.yaml max_concurrency` (+опц. пейсинг); + транспорт-ось, НЕ снапшот. `waves.workers` конфиг (НЕ `Fanout` — тот занят C2). +- Grep legacy DeepSeek-слагов в `backend/configs/` (прод не задет по плану — подтверди; eval-долг только + отметь в отчёте, eval/ не правь — чужая зона). + +**WS2 (план §2):** +- Бюджет draft-чанков в ВЫХОДНЫХ токенах: `est_out = 1.1978·cjk + 0.3852·other` (конфиг per-pair, + снапшот-folded `segmentationSnap`); классы символов — РЕАЛЬНЫЕ `unicode.RangeTable` из `EstimateTokens` + (НЕ ord-диапазоны скрипта — общность §0.1). Пин: `target_out=1797` → **56 чанков** на 25 главах. +- Edit-единица = глава; **`EditCeilingOut=3200` (ратифицированный дефолт)**; глава >3200 → абзацный сплит + (never-split-paragraph, lossless tiling). Пин: 25 глав → **37 единиц** (фикстура `ws2_chunk_sim.py`). + 8000-арм НЕ открывать (гейтнут Q2a). +- `Chunk` аддитивно: `EstOut`, `OversizedSentence` (мега-предложение = passthrough + флаг, БЕЗ клауза-сплита), + `EditUnitID`. Контракт Chapter/ChunkIdx/Text цел. +- Снос мёртвых `STMDepth`/`OverlapTokens` (`config/pipeline.go:47-48`) — carryover не строим. +- **src→dst-формат** `renderEditorConstraintBlock` (CONFIRMED-only дисциплина ЦЕЛА) + выделенный + **`render-format-version`** снапшот-компонент (НЕ `memoryMatchVersion`). +- Сайзинг max_tokens редактора: существующий механизм от `prev`-черновика (`stagerun.go:87-90`) — НОВОЙ + формулы НЕ вводить (пост-верифай F8); проверить достаточность под единицу-главу тестом. +- **Инъекция W2-единицы = СВЕЖИЙ `Select` по тексту ВСЕЙ единицы над enriched-банком** (sticky на уровне + глав вырожден); НЕ реюзить per-чанк memSel из W0 (тот над base-банком). Глоссарий-бюджет Select для + главы-единицы — открытый пункт §2(е): реши инженерно, при сомнении эскалируй. + +**WS3 (план §3):** +- `miner*.go`: порт V-A→V-C **строго по frozen exp16 `arms.py`** (точные пороги §3(б); детерминизм-контракт: + сорт-ключи, целые, версии, tie-break). **Лемматизатор: дефолт B** — Палладий-подканал в Go-дефолте + дропается, зависимые фикстуры пере-пинятся (古月 21→22); **Палладий-ИНВАРИАНТНЫЕ гарантии — EXACT: + 13618-кандидат-СЕТ (членство), recall 0.9649/0.9318, катастроф-скрин 方源/蛊/蛊师 ранги 0/1/2 ∈top-50.** +- Алиас-ярус-1 (R4-блоки ПЕРВЫМИ; юнит-фикстуры: 族长≠四代族长, компози-гард 古月+族长, кап-лемма-гейт + «найти»), консолидация §C2 (канон по ВСЕМ вхождениям; **майнер по построению НЕ пишет approved**), эмиссия + сид-дельты (схема `seedTerm` БЕЗ новых полей; эмиттер реюзит subsumption+fragment-фильтры, не сырой дамп). +- `Source:"mined"` (аддитивная миграция store) + mined-write-путь + `tmctl seed-lint` (сухой прогон + фейл-лаудов реального `loadGlossarySeed` по дельте). +- Паритет Go↔Python: **Python = референс**; при расхождении чинить Go; доказанный баг референса → эскалация. + +**WS4 (план §4):** +- Все 12 интеграционных точек с пин-тестами (список §4(д)). Несущие: срез `⟦TM-BANK-v1⟧` ДО `classifyOutput`; + derived-checkpoint ТОЧНАЯ формула с NULL-байтами (`"tm-banknote-v1\x00"+reqHash+"\x00"+stripped`); + **re-point `final_hash` OK-пути draft на derived — ПОСЛЕ резолва эскалации** (`stagerun.go:163`, потреблять + `last.stripped` — эскалированный черновик тоже очищен); `banknoteSnap{enabled, parser_version}` в + `snapshotID()`; инструкция сноски — В ФАЙЛ промпта переводчика (PromptSHA256 двигается); генерации + **finish=stop-only**; телеметрия `n_banknote_lines`/`banknote_parse_fail`/`banknote_truncated` лауд. +- Пин: пере-парс 95 строк exp16 = 0 fail (фикстура `ws4_banknote_verify.py`); толерантный обрез — синтетика. + +**WS5 (план §5):** +- Чекеры DC1 (时辰/единицы) / DC2 (масштабы чисел) / DC6 (регистр-негатив-лист) / DC7 (editor-инверсии + grade/role) — cheapgates-класс, target-gated, **наблюдаемость НЕ гейт** (landing гейтнут FP-замером §5(д) + на свежем прогоне). +- **DC3: инъекция пола — ВСЕ ТРИ состояния, вкл. hidden-мандат** (безродовые конструкции / surface-«он» до + `until_ch` — фикс класса Бай Нинбин D19.3); детерм. чекер — ТОЛЬКО male/female, паратекст-чанки исключены; + hidden-чекер НЕ строить (Ф2-кореференция). +- DC5-детекцию НЕ строить (74%/42% FP) — пак-локусы (данные) + слот сноски; рендер-политика за владельцем. +- Omission-бэкстоп: substring-матч `{dst, decl.forms, aliases}` + лёгкий детерминированный ru-стеммер + (Go-либа; БЕЗ pymorphy3); verdict-каскад и `inflection_gap`-флаг по плану. +- Данные пакета пары zh-ru (таблицы единиц, негатив-лист, verse-локусы) — версионируемый снапшот-folded пакет. +- `tmctl export --pairs` (source-join в Export уже есть) — пред-условие FP-замера DC1/DC2. + +**WS6 (план §6):** +- Армы редактора = КОНФИГ (yaml/models: glm-5 / mistral-large-latest / deepseek-v4-pro), снапшот per-арм, + атрибуция арма в export/report; `echo_mine_test` обновить под варианты; editor pinned (эскалации нет — D12). +- mistral-арм разблокируется rate-guard'ом из WS1 — проверь связку тестом (семафор реально ограничивает). + +## Свободы и рамки (решения владельца, 19.07) + +- **Рефакторинг/перенос кода по папкам — РАЗРЕШЁН и приветствуется:** архитектурная чистота важнее + минимальности диффа. Границы: инварианты 1–8, контракты плана и поведение вне скоупа не менять молча; + крупные структурные переносы — отдельным разделом отчёта (ревью-абельность лендинга). +- **Без велосипедов:** реюз stdlib и существующих механизмов репо (эталоны: `coverageSnap`-фолд, + derived-checkpoint, role-registry, `baseRequestLog`, `config→stage-list`). Новая внешняя зависимость — + только с обоснованием в отчёте (канон Go: минимум зависимостей). +- **Временные тесты/харнесы** — можно и нужно: `_test.go` рядом с кодом; одноразовые скрипты — в scratchpad, + НЕ в репо; временное помечай. +- **Баги по пути** — чинить разрешено, каждый в раздел «багфиксы вне плана» (лендинг скоуп-раздельный). +- **Не костылить:** честная реализация (б)-алгоритмов; хак «чтобы тест прошёл» запрещён; недоделка = + явная строка residual, не тихий пропуск. +- **Go-каноны:** `gofmt`/`go vet`/`-race` чисто; идиоматика (обёртка ошибок `%w`, контексты, без глобального + мутабельного стейта, table-driven тесты); комментарии — в стиле окружающего кода (констрейнты, не пересказ). +- **Неконсистентность** (план↔код↔доки↔вендор): НЕ гадать — даблчек всеми тулзами (grep по репо, git log, + доки, официальная вендор-дока/вебсёрч — правило двух направлений CLAUDE.md); реальное расхождение → СТОП + по этому куску + эскалация. Провайдер-аномалия → сначала вендор-дока, интерпретацию без сверки не вшивать. +- **$0 к провайдерам:** LLM-вызовы НЕ делать (ни облако, ни локаль) — вся верификация build/test/golden/ + фикстуры. Wire-корректность доказывается голденом и байт-диффами, не живыми вызовами. + +## Верификация (мандат самопроверки владельца 12.07 — ОБЯЗАТЕЛЕН; + чекпойнт-ревью) + +1. **Ревью исполнением непрерывно:** `go build ./... && go vet ./... && go test -race ./...` после каждого + куска; «зелёное» подтверждай ЛОГОМ, не словом (урок D37: «0 ошибок» ≠ 0 ошибок). +2. **Фикстуры плана — прогонять:** sticky-байты ≡ sequential · 56 чанков @1797 · 37 единиц @3200 · + miner-паритет (инвариантные EXACT; зависимые — пере-пин B) · банкнота 95/0 · kill-9 с N резервами · + seed-lint 0 фейл-лаудов · draft request_hash стабилен под обогащённым банком. +3. **Чекпойнт-ревью после блоков A/B/C** (адверсариальный воркфлоу, author≠reviewer, refute-by-default), три + линзы: (i) **алгоритм-vs-план** — реализация против (б)-секций построчно, детерминизм (сорты/целые/версии); + (ii) **сеньорское код-ревью** — Go-идиоматика, конкуренция, границы ошибок, чистота сеамов; + (iii) **функциональное** — деньги/resume/снапшот/гейты end-to-end на фикстурах. Находки чинить ДО + следующего блока; сводку каждого чекпойнта — в отчёт. +4. **Финал:** полный многоосевой адверсариал по паку (деньги/resume · снапшот/волны · детерминизм · + банк/банкнота · чекеры · экспорт · код-качество) + `PACK11_REPORT.md`. +5. Golden re-capture — только под ратифицированную смену поведения; маскированный дифф прикладывать всегда. + +## Что НЕ строить (гейтнуто/анти-скоуп — план §0/§11; в код не тащить) + +Крупно-главный арм >3200 out (Q2a-гейт; потолок остаётся конфигом) · наполнение `logical`-стратегии чанкера +(сеам без контента) · carryover/lookahead · diff-редактор · reflow-отдельным-пассом · W2.5-швейный пасс · +LLM-сегментация/ML-сцены/эмбеддинг-границы/RAG-оверлап · DC5-детерм-гейт · DC3-hidden-чекер · локальный +9B-споттер · жанр-паки · платные вызовы. Единый resnapshot — НЕ в этой сессии. + +## Эскалации и отчёт + +Настоящие развилки (конфликт плана с кодом, нереализуемость (б)-алгоритма, скоуп/деньги, дыры плана) — +эскалируй БЫСТРО короткой запиской через владельца (развилка + варианты + твоя рекомендация); это норма и +поощряется (образцы эры №6 — в D-логе), тихие девиации запрещены. Приёмка оркестратора после сдачи: +верификация исполнением (build/race/пины/golden-репликация) + адверсариальный рубеж-2 по сырью → лендинг → +единый resnapshot → пере-прогон 3–10 глав → чтение владельца (планка 2 претензии). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d8be92c..834078a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -4,11 +4,15 @@ > - **Фазы:** 0 ✅ · Ф1-инфра ✅ (D20–D28; golden = инвариант №8) · арка «качество-первым» D26→D38.5 ЗАКРЫТА (хроника — `archive/PROGRESS-2026-07-10-13.md`; итог: потолок в организации, не в моделях). **АРХ-РЕСЕТ D39 (13.07):** синк-аудит «сломанного телефона» (65 находок / 0 refuted — `architecture/08-sync-audit-ledger.md`) → целевая 7-слойная архитектура + фазовый план (`architecture/09-target-architecture.md`); инвариант общности §0.1 (любая книга/пара/структура); промпт-тема консолидирована в `architecture/10-prompt-architecture.md`. > - **Трек A (build-now) ЗАКРЫТ:** пак-1 (D39.2, `d3f6b34`: trust-gated suppressor [терм-дрейф закрыт в КОДЕ] · export-contract нормализация · pair-сеам `prompts:{zh-ru}` fail-loud · реестр role→инъекция · target-гейты · quality-report) + пак-1.5+F1–F9 (D39.5, `dc30c7d`: `tmctl export` [manifest-join+drift-гард; полигон-экстракция впредь ТОЛЬКО через него] · fold-first санитайзер [HIGH-обход закрыт] · bounded глосс-whitelist · пример zh→ru). Адверсариал-долг 529 погашен (D39.4: 8 осей, 0 CRIT/1 HIGH/6 MED — всё в фикс-листах/ledger). > - **Трек B (ресёрч) ЗАЛЕНДЕН:** research/19 нарезка+когезия+контракт t/e+волны W0–W3 (D39.1; конфликт exp14↔research/18 разрешён: эмиссия≠окно≠связность) · research/20 банк-майнинг W1.5 (D39.6; детектор-лестница V-A/B/C, алиас-ярусы, banknote-v1, плагины P1–P6, §D-пре-рег; конфаунд инъекции ⇒ cold-start срез). -> - **exp15 ЗАВЕРШЁН И ЗАЛЕНДЕН (D39.7, `fe34a16`):** «граница вредит» отозван (артефакт окна судьи, свинг +0.689 после фикса), все когезия-эффекты под шум-полом 0.126, «sequential нигде не лучше» (LOO-stable, НЕ ратифицированный зелёный свет волне), фертильность 1.20·cjk+0.39·other; $12.79/$15. **Слепое чтение ИСПОЛНЕНО (D39.8):** планка не пройдена никем, дефекты распределены по армам (читатель подтвердил floor-null; V2-брак = риг без гейтов, не carryover-рычаг) → битва качества пере-прогона = дефект-классы (时辰-юниты · числа b1 · gender-enforce · стих/аллюзии · регистр-лексикон), не нарезка. **Q4a ИСПОЛНЕН И ЗАЛЕНДЕН (D39.9, $1.32/$2):** верность НЕ покупается на translate-стадии (flash уже верен 25/0/27; pro ×3.67 не ратифицирован); смысл ломает glm-РЕДАКТОР (свап glm→mistral/deepseek-pro = приоритетный арм пере-прогона); exp14b-правила дефектны → $0-ре-аудит в exp16. **exp16 ИСПОЛНЕН И ЗАЛЕНДЕН (D39.10, $0.02/$10):** WHICH подтверждена (код-детектор V-C recall 0.97 held-out, $0) / WHAT опровергнута для ко-оккуренции → dst-канал = банкнота (идея владельца оправдана) + облако+Палладий; 9B-споттер не нужен (0.895 ≤ код); Z1 → облако; ре-аудит exp14b: ядро D38 держится, errata не нужна. Тачпойнты владельцу: карта подписи (пол) · мини-голд алиасов · precision@30 (books/gu-zhenren/exp16/). **Исследовательская программа ЗАВЕРШЕНА.** **Директива владельца (D39.11): стройка ТОЛЬКО после ДИЗАЙН-СИНТЕЗ сессии** (синтез всех находок → план реализации с верифицированными алгоритмами → ратификация → бэкенд-промт). **Очередь:** дизайн-синтез (промт ВЫДАН 19.07 — `DESIGN_SYNTHESIS_SESSION_PROMPT.md`, ждёт старта владельцем) → ревью/ратификация плана → бэкенд-пак слоёв 1+4 → → полигон-стадия банк-майнинга (§D research/20, отдельный промт ПОСЛЕ exp15) → единый resnapshot (D30.9, одна переоплата; вкл. RegressionGuard-тумблер D38.4) → пере-прогон 3–10 глав → чтение владельца (интерим-планка 2 претензии, D35).残: POLYGON_PACKAGE4 residual-трекер (пилот/18+/echo — отложен, D36.1). +> - **exp15 ЗАВЕРШЁН И ЗАЛЕНДЕН (D39.7, `fe34a16`):** «граница вредит» отозван (артефакт окна судьи, свинг +0.689 после фикса), все когезия-эффекты под шум-полом 0.126, «sequential нигде не лучше» (LOO-stable, НЕ ратифицированный зелёный свет волне), фертильность 1.20·cjk+0.39·other; $12.79/$15. **Слепое чтение ИСПОЛНЕНО (D39.8):** планка не пройдена никем, дефекты распределены по армам (читатель подтвердил floor-null; V2-брак = риг без гейтов, не carryover-рычаг) → битва качества пере-прогона = дефект-классы (时辰-юниты · числа b1 · gender-enforce · стих/аллюзии · регистр-лексикон), не нарезка. **Q4a ИСПОЛНЕН И ЗАЛЕНДЕН (D39.9, $1.32/$2):** верность НЕ покупается на translate-стадии (flash уже верен 25/0/27; pro ×3.67 не ратифицирован); смысл ломает glm-РЕДАКТОР (свап glm→mistral/deepseek-pro = приоритетный арм пере-прогона); exp14b-правила дефектны → $0-ре-аудит в exp16. **exp16 ИСПОЛНЕН И ЗАЛЕНДЕН (D39.10, $0.02/$10):** WHICH подтверждена (код-детектор V-C recall 0.97 held-out, $0) / WHAT опровергнута для ко-оккуренции → dst-канал = банкнота (идея владельца оправдана) + облако+Палладий; 9B-споттер не нужен (0.895 ≤ код); Z1 → облако; ре-аудит exp14b: ядро D38 держится, errata не нужна. Тачпойнты владельцу: карта подписи (пол) · мини-голд алиасов · precision@30 (books/gu-zhenren/exp16/). **Исследовательская программа ЗАВЕРШЕНА.** **Директива владельца (D39.11): стройка ТОЛЬКО после ДИЗАЙН-СИНТЕЗ сессии** (синтез всех находок → план реализации с верифицированными алгоритмами → ратификация → бэкенд-промт). **Очередь:** ✅ дизайн-синтез ИСПОЛНЕН, план РАТИФИЦИРОВАН (**D39.12** — `architecture/11-implementation-plan.md`, три рубежа верификации, 4 гейтнутых арма) → **бэкенд-пак ЕДИНОЙ сессией (промт ВЫДАН — `BACKEND_PLAN11_SESSION_PROMPT.md`; решение владельца)** → приёмка оркестратора → единый resnapshot (D30.9, одна переоплата; §8-манифест плана; вкл. RegressionGuard-тумблер D38.4) → пере-прогон 3–10 глав (армы редактора glm/mistral/deepseek-pro) → чтение владельца (интерим-планка 2 претензии, D35).残: POLYGON_PACKAGE4 residual-трекер (пилот/18+/echo — отложен, D36.1). > - **Стек (не менялся с D38.5):** draft deepseek-v4-flash (thinking ON) → editor glm-5 БИЛИНГВ v3-discourse (P1a+чэнъюй+few_shot-тумблер; свап-кандидаты mistral/deepseek-pro = армы пере-прогона) → судья gemini-3.1-pro-preview; канал B Mistral+grok; 7 ключей; ~$0.85/ранобэ (D30.4). > - **Ждём от владельца:** запуск exp15 (бюджет ≤$15 подтверждён) · research/20 §E: мини-голд алиасов (~20–30 мин) / бюджет ≤$5 полигон-стадии / политика канона / жанр-паки / реплика ja→ru · чтение пере-прогона (после очереди) · старые висящие: планка запуска (лучше-фана/гибрид/издательский) · билингв-якорь пилота (D25.9-Q1) · контаминация пилот-корпуса (D27.4) · publishable/waiver (D25.1) · FN-bound L3 (D25.4) · юр-пакет · провенанс 12-*-доков. > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `archive/PROGRESS-2026-07-10-13.md` (D39.6-гигиена). Записи ниже — живая эра D39. +## Оркестратор №7 — план 11 РАТИФИЦИРОВАН (D39.12), бэкенд-промт единого пака ВЫДАН (19.07, вторая половина сессии) + +Дизайн-синтез сдал план (1066 строк) после трёх рубежей: ревью-2 оркестратора (F1–F11: 3 MAJOR соло-прохода + 2 замечания владельца [mistral rate-guard ~48% retry-fails · полы per-editor-model] + Q9-провенанс) → 7-агентный пост-фикс адверсариал сессии (5 находок: F8 ПЕРЕПИСАН — сайзинг уже от `prev`; дефолт 3200 → 37 edit-единиц; mined-write-путь [`loadGlossarySeed` хардкодит seed]; render-format-version реконсиляция; F6 усилен narrative-coref) → 3-агентный подтверждающий пас (2 резидуала закрыты). Спот-чек внесения — исполнением (36 маркеров, grep всех фиксов). **Залендено одним пакетом:** план с ревью-шапкой + `eval/design11/` (7 скриптов, 6 верификаций PASS) + quirks-запись mistral (из §12-заготовки, зона полигона — с пометкой) + D39.12 + архивация спент-промта дизайн-синтеза. **Гейтнутые армы: 4** (edit>3200 за Q2a · качество свап-арма за платным полигоном · DC-landing за $0-FP · mistral за rate-guard). Развилки §10 (14 шт., с рекомендациями) — у владельца, стройку не блокируют. **Выдан `BACKEND_PLAN11_SESSION_PROMPT.md`** — единый пак WS1–WS6 блоками A/B/C/D с чекпойнт-ревью (решение владельца: одной сессией; свободы: рефакторинг/перенос кода, временные тесты, багфиксы со скоуп-пометкой; Go-каноны; даблчек неконсистентностей; $0 к провайдерам). Очередь: бэкенд-сессия → приёмка (рубеж-2) → единый resnapshot → пере-прогон → чтение. + ## Оркестратор №7 — промт ДИЗАЙН-СИНТЕЗ сессии выдан (deliverable №1 по D39.11), 2026-07-19 Онбординг по хендоффу №5 (D-лог D39–D39.11 + несущие отчёты: research/19 §B/§C/§E, research/20 §B/§C/§E, exp15 §3/§7/§7.9/§7-Q4a, exp16 целиком). **Выдан `docs/DESIGN_SYNTHESIS_SESSION_PROMPT.md`** — проект-ресёрч сессия синтеза всех находок в ратифицируемый план `architecture/11-implementation-plan.md`. Скоуп: WS1 волновой раннер (деньги/SQLite/гонка `Runner.clients`/sticky-A5-цепочка + scheduler-волны + три вендор-чека: deepseek пик-окна D39.10-6, кэш ZAI/GLM §E.8-бис, слаги 24.07) · WS2 фертильность-бюджет + крупная edit-единица (+ omission-бэкстоп; src→dst D30.1 решается здесь, парковка запрещена) · WS3 Go-майнер W1.5 (V-C + алиасы + консолидация + сид-дельта §C2-7 + карта подписи) · WS4 банкнота×12 гейт-точек (вкл. re-point final_hash) · WS5 дефект-класс чекеры D39.8 + данные пакета пары · WS6 свап-арм редактора + готовность пере-прогона (риг-стандарт D39.7 вшит); глобальные секции: порядок пакетов, манифест ЕДИНОГО resnapshot (D30.9, вкл. gate-условную семантику банка), диспозиции хвостов (ledger-очередь, §E-слоты, L7-NEVER_CLOSED: H7/H8/H10/H16), реестр вопросов владельцу. Каждый WS — верификация ДО стройки ($0-симуляции, скрипты `eval/design11/`, числа в план); мандат самопроверки + адверсариальный селф-ревью + completeness-critic L1–L8; $0 жёстко (платная несущая верификация = эскалация-развилка, не тихий деферал «на после стройки»). **Промт прошёл адверсариальное ревью-воркфлоу до выдачи (5 линз × refute-by-default, 25 агентов): 20 CONFIRMED-находок (4 MAJOR), все внесены** — несущие: gate-условная снапшот-семантика auto/draft (`memory.go:259-266`); пропущенные D12-риски параллелизма (гонка clients, sticky-цепочка → request_hash); lookahead/Q2c переведены из «доказано не строим» в парковку «не тестировано»; verify-before-build маршрут платных верификаций; мега-предложение — НЕ «уже в коде» (oversize-passthrough без флага). file:line-якоря спот-верифицированы по HEAD исполнением. ⚠ Док-синк заметка (не чинил — вне минимального скоупа): ярлык «инвариант №8» перегружен — backend/README №8 = экспорт-контракт (с 17.07), CLAUDE.md/шапка D-лога зовут №8 golden (эпоха D23); развести при следующей актуализации статус-доков. Следующее: владелец стартует дизайн-сессию → адверсариальное ревью+ратификация плана (D-блок) → бэкенд-промт строго по плану. diff --git a/docs/README.md b/docs/README.md index e5a501b..30c797f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,11 +12,11 @@ ## Структура - `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.6); при конфликте с любым доком он выше.** - - `01-decisions.md` — принципы Р1–Р10; `02-mvp-plan.md` — фазы и приёмка (v3, 09.07); `03-implementation-notes.md` — контракты Фазы 0; `04-unhappy-paths.md` — ~70 режимов отказа → механизм; `06-memory-risk-registry.md` — реестр рисков банка памяти; **[`07-strategic-review.md`](architecture/07-strategic-review.md) — стратегический аудит (09.07): вердикт end-to-end, топ-риски, курс-коррекции**; **[`08-sync-audit-ledger.md`](architecture/08-sync-audit-ledger.md) — верифицированный ледджер синк-аудита «сломанного телефона» (65 находок, D39)** · **[`09-target-architecture.md`](architecture/09-target-architecture.md) — целевая 7-слойная архитектура + фазовый план арх-ресета (D39; инвариант общности §0.1)** · [`10-prompt-architecture.md`](architecture/10-prompt-architecture.md) — консолидированная промпт-заметка (концерн 4); `components.puml`/`pipeline.puml` — диаграммы v3 (владелец смотрит PlantUML-расширением VS Code; вручную НЕ рендерить). + - `01-decisions.md` — принципы Р1–Р10; `02-mvp-plan.md` — фазы и приёмка (v3, 09.07); `03-implementation-notes.md` — контракты Фазы 0; `04-unhappy-paths.md` — ~70 режимов отказа → механизм; `06-memory-risk-registry.md` — реестр рисков банка памяти; **[`07-strategic-review.md`](architecture/07-strategic-review.md) — стратегический аудит (09.07): вердикт end-to-end, топ-риски, курс-коррекции**; **[`08-sync-audit-ledger.md`](architecture/08-sync-audit-ledger.md) — верифицированный ледджер синк-аудита «сломанного телефона» (65 находок, D39)** · **[`09-target-architecture.md`](architecture/09-target-architecture.md) — целевая 7-слойная архитектура + фазовый план арх-ресета (D39; инвариант общности §0.1)** · [`10-prompt-architecture.md`](architecture/10-prompt-architecture.md) — консолидированная промпт-заметка (концерн 4); **[`11-implementation-plan.md`](architecture/11-implementation-plan.md) — РАТИФИЦИРОВАННЫЙ план стройки пере-прогонного стека (D39.12; дизайн-оф-рекорд бэкенд-пака, три рубежа верификации)**; `components.puml`/`pipeline.puml` — диаграммы v3 (владелец смотрит PlantUML-расширением VS Code; вручную НЕ рендерить). - `experiments/` — эмпирика «Полигона»: `00-provider-quirks` (читать перед любым вызовом провайдера), `01-token-calibration`, `02-refusal-benchmark`, `03-local-stand`, `04-editor-quality`, `06-local-extraction`, `07-coverage-precision`, `08-cost-model-v2` (актуальная денежная модель), `09-pilot-protocol` (пилот Ф2.5 + поправки D13), `10-explicit-benchmark` (18+ violence-рука канала B), `11-erotica-benchmark` (erotica по трём парам/регистрам — закрытие D14.4, D22). - `research/` — фактура исследований 04–05.07: `01–10` базовые, `11-gap-*` добор критиком, `12-*` режимы отказа/отзывы/таксономии (+ два внешних материала с провенанс-шапками), `13` валидация памяти, `14` адаптивная память, `15` голос и состояние (принят, D21), **`16` ридер-IDE (принят с ревью-шапкой, D29)**, **`17` внешняя критика GPT-5.6 (принят с ревью-шапкой, D25)** — у 16/17 читать шапку прежде тела. **`18` рычаги качества (два отчёта, D36)** · **`19` нарезка+когезия+контракт t/e (D39.1)** · **`20` банк-майнинг W1.5 (D39.6)** — у всех ревью-шапки. ⚠ Часть под superseded-баннерами (01/02/03/04/05/09 и gap-1/2/5) — **читай баннер прежде содержимого**. - `PROGRESS.md` — **журнал** (CURRENT-STATE сверху, ниже хронология; НЕ источник решений). -- **Активные хендофф-промты (пост-D39.11, 19.07):** [`DESIGN_SYNTHESIS_SESSION_PROMPT.md`](DESIGN_SYNTHESIS_SESSION_PROMPT.md) (**СТАРТОВЫЙ для дизайн-синтез сессии — очередь D39.11**; выход — `architecture/11-implementation-plan.md`) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №5 оркестратора) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). Закрытые — в `archive/prompts/` (свежие: exp16→D39.10, Q4a→D39.9, exp15→D39.7, банк-майнинг-ресёрч→D39.6). +- **Активные хендофф-промты (пост-D39.12, 19.07):** [`BACKEND_PLAN11_SESSION_PROMPT.md`](BACKEND_PLAN11_SESSION_PROMPT.md) (**СТАРТОВЫЙ для бэкенд-сессии — единый пак по ратифицированному плану [`11-implementation-plan.md`](architecture/11-implementation-plan.md), D39.12**) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №5 оркестратора) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). Закрытые — в `archive/prompts/` (свежий: дизайн-синтез→D39.12). Закрытые — в `archive/prompts/` (свежие: exp16→D39.10, Q4a→D39.9, exp15→D39.7, банк-майнинг-ресёрч→D39.6). - `archive/` — закрытые сессионные промты (только история, инструкции оттуда не исполнять). ## Статус (2026-07-12, пост-D38.1 · консолидация D38.2) diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 32628ec..ec9c729 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -3,7 +3,7 @@ > **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто: > - **Полностью superseded:** **D1 (моно-редактор) → D17 → D30.1 (редактор БИЛИНГВ)** · D9 (ja-приёмка) → D18 (蛊真人 zh→ru; ja — второй прогон) · D17 → D30.1 · скобка D19.4 «ключ без data-sharing» и D20.3 «чистый ключ = блокер пилота» → **D27** (единый ключ С data-sharing, отключение перед продом) · exp04-дефолт «editor grok-4.3» (D3) → D30.1 (grok reasoning-off из редакторских ролей СНЯТ — no-op; кандидат glm-5-билингв; gemini — премиум-эскалация только за санитайзером D30.3). > - **Частично амендировано:** D3 (канал B → D14.1/D19.1 + оговорки D22.5/D22.6; апекс-слаг `-preview` — D22.4; draft под вопросом exp13 — D30.6) · D6 («off/minimal для draft/edit» эродирован: draft thinking-ON принудительно, editor-off снят D30.1; живы per-роль принцип и D6.2-буфер) · D10 (+D24.2 alias-слепое пятно, истинная консистентность ≈99.5%) · D11-числа → exp08 → **D30.4** (флип D1 = +15–25%, ~$0.85/ранобэ; «$1.5–2» = неподписанная опция Б) · D12 (+D24.3 флоры max_tokens; +D29.1в rollup — амендмент вердикт-правила) · D13.1-арм из решающего → ПОДТВЕРЖДАЮЩИЙ (D30.1); +D25.3 prefix-анализ судьи · D14.2 закрыт D19.1/D22.4; D14.4 закрыт D22.1 · D15.3 исполнен; спека D15.2: v2→v3→v3.1 (D22.2), реализация = текущий пакет (D30.9) · D24.4 +D28.1 (precision/recall-трейдофф; hard-gate требует пере-замера) · порядок D24.5 отложен D26.1 (этап B — после читаемых 25 глав). -> - **Живое ядро без изменений:** D2 (+новый гейт-класс D30.3), D4, D5, D7, D8, D10-механизм, D12-таксономия отказов, D16, D18, D19.1–19.2, D21 (Ф2-механизмы voice/address/reveal), D22.5–22.7, D23 (golden = инвариант №8), D24–D39.11 — действующая голова контракта (**D39.11 = директива владельца: стройка ТОЛЬКО после дизайн-синтез сессии с верифицированными алгоритмами; хендофф №5**; **D39.10 = exp16: WHICH подтверждена [V-C recall 0.97, $0] / WHAT→банкнота+Палладий [canon-recovery <70%], 9B-споттер не нужен, Z1→облако, ре-аудит exp14b чист [errata не требуется]; исследовательская программа ЗАВЕРШЕНА → бэкенд-пак слоёв 1+4**; **D39.9 = Q4a: верность НЕ на translate-стадии [flash уже верен, pro ×3.67 не ратифицирован], смысл ломает glm-РЕДАКТОР [свап-арм приоритетен], DET-пол 0.261; exp14b-правила дефектны → $0-ре-аудит в exp16**; **D39.8 = слепое чтение: планка не пройдена никем, дефекты распределены [читатель подтвердил floor-null], битва качества → дефект-классы [时辰-юниты, числа, gender-enforce, стих, регистр]**; **D39.7 = exp15 залендён REV.2: «граница вредит» отозван [артефакт окна судьи], все когезия-эффекты под floor 0.126, «sequential нигде не лучше» LOO-stable [не ратифицированный зелёный свет], фертильность 1.20/0.39, $12.79/$15; гейт = слепое чтение владельца**; **D39.6 = research/20 банк-майнинг залендён [детектор-лестница V-A/B/C, алиас-ярусы, banknote-v1 с 12 гейт-точками, §B5 плагины P1–P6, §D-пре-рег]; полигон-стадия после exp15**; **D39.5 = пак-1.5+F1–F9 залендены [tmctl export с manifest/drift-гардами · fold-first санитайзер · bounded глосс · пример zh-ru]; экстракция полигона впредь через tmctl export**; **D39.4 = адверсариал-долг ПОГАШЕН [8 осей: 0 CRIT/1 HIGH/6 MED], пак-1.5 придержан под фикс-лист F1–F9 в §T4**; **D39.3 = пакет банк-майнинга W1.5 [диспозиция H15/V4-п4] ратифицирован, research/20-промт с HOLD до фриза exp15**; **D39.2 = трек-A пак-1 залендён `d3f6b34`: trust-gated suppressor [терм-дрейф закрыт в КОДЕ] + export-contract нормализация + pair-сеам + реестры + quality-report; адверсариал-долг за 529**; **D39.1 = research/19 залендён: конфликт exp14↔research/18 разрешён [эмиссия≠окно≠связность], Go-спека чанкера+когезии, волновая архитектура W0–W3 [вводная владельца: параллелизм], пре-рег эмпирики Q0–Q5; оркестратор-ревью снято владельцем**; **D39 = АРХ-РЕСЕТ: синк-аудит сломанного телефона [65 находок, 0 refuted, `08-sync-audit-ledger.md`] + целевая 7-слойная архитектура [`09-target-architecture.md`] + фазовый план [трек A build-now ∥ трек B ресёрч, пере-прогон после]; формализует фантомный «D35.7» как цель слоя 1; терм-дрейф код-корень жив [сид-воркэраунд]; редактор 4-мандата-full-regen = причина инверсий**; D35 = пивот качество-первым; D36 = приёмка research/18; D37 = приёмка exp14: претензия-1=промпт-рычаг; D38 = приёмка exp14b: «только gpt-5.4» ОПРОВЕРГНУТО [mistral/deepseek-pro чинят], фронтир в дефолт НЕ нужен, корень терм-дрейфа = статус-draft сида; **D38.1 = слепой h2h P1a↔F-disc залендён [ChatGPT «фронтир сильнее» / Claude «near-parity» расходятся, СХОДЯТСЯ: дефекты = инфра] → РАЗВОРОТ «мерить→строить», выдан BACKEND_INFRA_PACK; D38.2 = консолидация/док-гигиена: спент exp-скрипты в `eval/exp12|13|14|14b/`, статус-доки+.puml актуализированы; D38.3 = инфра-пак читаемости залендён (санитайзер-классы strip+export + число/omission-гард), верифицирован исполнением [build/vet/test зелёные, 3 span-дефекта запинены]; D38.4 = аудит полноты exp14/14b: чэнъюй-рычаг забыт near-term→дописан в дискурс-промпт; inversion-гард D37 §2г не строился (число/omission ≠ полярность)→editor-swap = защита от инверсий (приоритет↑); нарезка = re-run-арм; гард-тумблер вкл. при resnapshot; D38.5 = компоненты resnapshot залендены+верифицированы [reseed-сид eb409f2 · дискурс+чэнъюй editor v3 0ac5f7a с few_shot-тумблером] → следующий шаг единый resnapshot + пере-прогон**). +> - **Живое ядро без изменений:** D2 (+новый гейт-класс D30.3), D4, D5, D7, D8, D10-механизм, D12-таксономия отказов, D16, D18, D19.1–19.2, D21 (Ф2-механизмы voice/address/reveal), D22.5–22.7, D23 (golden = инвариант №8), D24–D39.12 — действующая голова контракта (**D39.12 = план `architecture/11-implementation-plan.md` РАТИФИЦИРОВАН [три рубежа: ревью-2 F1–F11 → 7-агентный адверсариал → подтверждающий пас; пер-волновой снапшот + инвариант секвенирования = «переоплата ОДНА»; EditCeilingOut=3200; лемматизатор дефолт B; 4 гейтнутых арма]; бэкенд-пак ЕДИНОЙ сессией — `BACKEND_PLAN11_SESSION_PROMPT.md`**; **D39.11 = директива владельца: стройка ТОЛЬКО после дизайн-синтез сессии с верифицированными алгоритмами; хендофф №5**; **D39.10 = exp16: WHICH подтверждена [V-C recall 0.97, $0] / WHAT→банкнота+Палладий [canon-recovery <70%], 9B-споттер не нужен, Z1→облако, ре-аудит exp14b чист [errata не требуется]; исследовательская программа ЗАВЕРШЕНА → бэкенд-пак слоёв 1+4**; **D39.9 = Q4a: верность НЕ на translate-стадии [flash уже верен, pro ×3.67 не ратифицирован], смысл ломает glm-РЕДАКТОР [свап-арм приоритетен], DET-пол 0.261; exp14b-правила дефектны → $0-ре-аудит в exp16**; **D39.8 = слепое чтение: планка не пройдена никем, дефекты распределены [читатель подтвердил floor-null], битва качества → дефект-классы [时辰-юниты, числа, gender-enforce, стих, регистр]**; **D39.7 = exp15 залендён REV.2: «граница вредит» отозван [артефакт окна судьи], все когезия-эффекты под floor 0.126, «sequential нигде не лучше» LOO-stable [не ратифицированный зелёный свет], фертильность 1.20/0.39, $12.79/$15; гейт = слепое чтение владельца**; **D39.6 = research/20 банк-майнинг залендён [детектор-лестница V-A/B/C, алиас-ярусы, banknote-v1 с 12 гейт-точками, §B5 плагины P1–P6, §D-пре-рег]; полигон-стадия после exp15**; **D39.5 = пак-1.5+F1–F9 залендены [tmctl export с manifest/drift-гардами · fold-first санитайзер · bounded глосс · пример zh-ru]; экстракция полигона впредь через tmctl export**; **D39.4 = адверсариал-долг ПОГАШЕН [8 осей: 0 CRIT/1 HIGH/6 MED], пак-1.5 придержан под фикс-лист F1–F9 в §T4**; **D39.3 = пакет банк-майнинга W1.5 [диспозиция H15/V4-п4] ратифицирован, research/20-промт с HOLD до фриза exp15**; **D39.2 = трек-A пак-1 залендён `d3f6b34`: trust-gated suppressor [терм-дрейф закрыт в КОДЕ] + export-contract нормализация + pair-сеам + реестры + quality-report; адверсариал-долг за 529**; **D39.1 = research/19 залендён: конфликт exp14↔research/18 разрешён [эмиссия≠окно≠связность], Go-спека чанкера+когезии, волновая архитектура W0–W3 [вводная владельца: параллелизм], пре-рег эмпирики Q0–Q5; оркестратор-ревью снято владельцем**; **D39 = АРХ-РЕСЕТ: синк-аудит сломанного телефона [65 находок, 0 refuted, `08-sync-audit-ledger.md`] + целевая 7-слойная архитектура [`09-target-architecture.md`] + фазовый план [трек A build-now ∥ трек B ресёрч, пере-прогон после]; формализует фантомный «D35.7» как цель слоя 1; терм-дрейф код-корень жив [сид-воркэраунд]; редактор 4-мандата-full-regen = причина инверсий**; D35 = пивот качество-первым; D36 = приёмка research/18; D37 = приёмка exp14: претензия-1=промпт-рычаг; D38 = приёмка exp14b: «только gpt-5.4» ОПРОВЕРГНУТО [mistral/deepseek-pro чинят], фронтир в дефолт НЕ нужен, корень терм-дрейфа = статус-draft сида; **D38.1 = слепой h2h P1a↔F-disc залендён [ChatGPT «фронтир сильнее» / Claude «near-parity» расходятся, СХОДЯТСЯ: дефекты = инфра] → РАЗВОРОТ «мерить→строить», выдан BACKEND_INFRA_PACK; D38.2 = консолидация/док-гигиена: спент exp-скрипты в `eval/exp12|13|14|14b/`, статус-доки+.puml актуализированы; D38.3 = инфра-пак читаемости залендён (санитайзер-классы strip+export + число/omission-гард), верифицирован исполнением [build/vet/test зелёные, 3 span-дефекта запинены]; D38.4 = аудит полноты exp14/14b: чэнъюй-рычаг забыт near-term→дописан в дискурс-промпт; inversion-гард D37 §2г не строился (число/omission ≠ полярность)→editor-swap = защита от инверсий (приоритет↑); нарезка = re-run-арм; гард-тумблер вкл. при resnapshot; D38.5 = компоненты resnapshot залендены+верифицированы [reseed-сид eb409f2 · дискурс+чэнъюй editor v3 0ac5f7a с few_shot-тумблером] → следующий шаг единый resnapshot + пере-прогон**). Ответ на вопросы бэкенд-сессии (PROGRESS §«Вопросы от бэкенда», §«[НУЖНО РЕШЕНИЕ] перед Фазой 1» — с ревизии D31 в `archive/PROGRESS-2026-07-04-10.md`) и полигона (think-режим). Каждое решение прошло адверсариальную панель из 3 критиков (research / экономика / исполнимость в коде Фазы 0); ни одно не отклонено, все уточнены. Это **контракт Фазы 1** — бэкенд исполняет отсюда; при конфликте с буквой 01-decisions/02-mvp-plan — источник истины здесь (потом сольём в основные доки). @@ -662,3 +662,13 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу **Процесс-решение владельца (ратифицирую дословно):** в бэкенд «просто так не бросаться». Между завершённой исследовательской программой (D39.1–D39.10) и бэкенд-паком вставляется **проект-ресёрч сессия дизайн-синтеза**: синхронизировать ВСЕ находки (research/19–20, exp15/16, аудит 08, дефект-классы слепого чтения) → выработать чёткий план реализации «что и как меняем» с **разработанными и верифицированными алгоритмами** (self-проверки в каждом шаге) → ратификация плана → только затем промт бэкенда → бэкенд реализует. Это финальное закрытие концерна «тактическое латание»: стройка только по ратифицированному плану. **Хендофф:** оркестратор №6 закрывает сессию (арка D39–D39.10 исполнена целиком: синк-аудит → 7-слойная архитектура → трек A построен → трек B исследован → exp15/exp16 закрыты с двухраундовой верификацией). Роль передаётся хендоффом №5 (`ORCHESTRATOR_SESSION_PROMPT.md` переписан начисто; №4 — в архив). **Первый deliverable №5 — промт дизайн-синтез сессии.** Висящие тачпойнты владельца (карта подписи/пол · мини-голд алиасов · precision@30 · старые решения) и опция ja→ru-реплики — в хендоффе. + +## D39.12 — План реализации (`architecture/11-implementation-plan.md`) РАТИФИЦИРОВАН после трёх рубежей; бэкенд-пак — ЕДИНОЙ сессией (19.07, оркестратор №7). ✅ + +Дизайн-синтез сессия исполнила D39.11-промт (выдан `766a60e` после 20-находочного адверсариального ревью самого промта): **`docs/architecture/11-implementation-plan.md`** (1066 строк: WS1 волновой раннер · WS2 чанкер-бюджет+edit-единицы · WS3 Go-майнер W1.5 · WS4 банкнота×12 · WS5 дефект-чекеры+пакет-данные · WS6 армы; §7 порядок · §8 манифест единого resnapshot · §9 диспозиции L1–L8+H5–H19+парковки · §10 реестр 14 вопросов владельцу · §11 самопроверка · §12 quirks-заготовка) + `eval/design11/` (7 $0-скриптов, шесть верификаций PASS, детерминированы). + +**Верификация трёхрубежная:** (1) **ревью-2 оркестратора** (соло по лимит-констрейнту владельца — полное чтение + file:line-спот-чеки по HEAD исполнением): 8 находок [3 MAJOR: противоречие дефолтов `EditCeilingOut` 8000↔3200 · скоуп подписи W1.5 не запинен · инъекция W2 для edit-единиц не специфицирована] + 2 замечания владельца [per-model rate-guard mistral (~48% retry-fails, exp15 §7.6 REV.2) · полы per-editor-model/per-материал] + Q9-провенанс [клейм D39.7 «официально» — через владельца без пина; свежий чек 19.07: офиц. прайс ФЛЭТ → ×2 остаётся риск-пином до ре-чека 24.07]. (2) **Пост-фикс 7-агентный адверсариал сессии** (мульти-агентный рубеж, которого ревью-2 не гоняло): все 11 фиксов подтверждены, 5 новых находок впитаны — **F8 ПЕРЕПИСАН** (моя формулировка «сайзить от est_out» была неверна: `stagerun.go:87-90` уже сайзит редактора от черновика `prev`) · F1 пере-симулирован (**ратифицированный дефолт 3200 → 25 глав = 37 edit-единиц**, фикстура) · **mined-write-путь** (`loadGlossarySeed` хардкодит `Source:seed` → дельта персистится отдельным путём со штампом `Source:mined`; промоут 甲乙丙丁/四代族长 = pre-run reseed, НЕ W1.5) · снапшот-loudness реконсилирован к выделенному `render-format-version` · F6 усилен (2-я DC3-сработка = narrative-coref на hidden → довод к Ф2 сильнее). (3) **3-агентный подтверждающий пас:** 2 резидуала реконсиляции закрыты. Спот-чек внесения при лендинге — исполнением (36 маркеров, все фиксы найдены grep'ом). + +**Ратифицирую как дизайн-оф-рекорд:** пер-волновой снапшот (`base-bank` = approved `Source∈{seed,ruby}` / `enriched-bank` = все approved) + **инвариант секвенирования** (W2 никогда не пере-рендерит draft-стадию; черновик по `final_hash`) ⇒ «переоплата ОДНА» доказуема при обоих состояниях PostcheckGate · `EditCeilingOut=3200` · src→dst-формат констрейнт-блока редактора (закрытие D30.1) · лемматизатор — дефолт B (дроп Палладий-подканала, пере-пин 古月→22, катастроф-скрин PASS) · банкнота 12 точек (`banknoteSnap`, re-point ПОСЛЕ резолва эскалации) · DC3-инъекция всех трёх состояний вкл. hidden-мандат (hidden-чекер → Ф2) · DC5 → пак-локусы (детерм. детекция 74%/42% FP — не лендится) · mistral rate-guard (quirks-запись внесена из §12). **Гейтнутые армы (4, стройка сердцевины не ждёт):** крупная edit-единица >3200 (платный Q2a, объединить с полигон-прогоном) · качество свап-арма (платный полигон) · DC-чекеры landing ($0-FP на свежем прогоне) · mistral-арм (rate-guard). Развилки §10 — у владельца, стройку НЕ блокируют (дефолты консервативны; смена решения = конфиг/аддитив). + +**Решение владельца (19.07): бэкенд-пак — ОДНОЙ сессией** (не паки 2/3/4 порознь), блоками A/B/C/D с чекпойнт-ревью между; свободы сессии: рефакторинг/перенос кода разрешён (архитектурная чистота, без велосипедов), временные тесты, багфиксы по пути со скоуп-пометкой; Go-каноны; неконсистентности — даблчек всеми тулзами + эскалация; $0 к провайдерам. **Промт выдан: `BACKEND_PLAN11_SESSION_PROMPT.md`.** Очередь после пака: приёмка оркестратора (рубеж-2) → единый resnapshot (§8) → пере-прогон 3–10 глав (армы редактора) → чтение владельца (планка 2 претензии). Спент-промт дизайн-синтеза архивирован с баннером. diff --git a/docs/architecture/11-implementation-plan.md b/docs/architecture/11-implementation-plan.md new file mode 100644 index 0000000..31a830e --- /dev/null +++ b/docs/architecture/11-implementation-plan.md @@ -0,0 +1,1069 @@ +# План реализации пере-прогонного стека (слои 1+4+5 + данные слоя 2) — дизайн-синтез, 2026-07-19 + +> **Статус: РАТИФИЦИРОВАН — D39.12 (19.07, оркестратор №7); дизайн-оф-рекорд бэкенд-стройки.** Проектный +> документ дизайн-синтез-сессии (D39.11): синтез всей исследовательской программы арх-ресета (research/19–20, +> exp15/16, аудит 08, дефект-классы слепого чтения) в план стройки с верифицированными ($0) алгоритмами. +> **Ревью-провенанс (три рубежа):** ревью-2 оркестратора (F1–F11, все внесены — 36 маркеров) → пост-фикс +> 7-агентный адверсариал-воркфлоу сессии (5 находок впитаны, вкл. переписанный F8 и mined-write-путь) → +> 3-агентный подтверждающий пас (2 резидуала закрыты). Бэкенд строит СТРОГО по этому плану +> (`BACKEND_PLAN11_SESSION_PROMPT.md`, единый пак — решение владельца); **4 гейтнутых арма — §11**. +> +> **Источник истины:** при конфликте побеждает D-лог (`05-decisions-log.md`, D1–D39.11). Все `file:line` — +> против HEAD `766a60e`. Все пороги/коэффициенты — число + источник. $0-верификации — `eval/design11/`, +> результаты в `books/gu-zhenren/design11/` (вне git); сводка — `VERIFICATION_RESULTS.md`. +> +> **Дисциплина цитат:** аудит 08 и D-лог местами несут устаревшие `memory.go`-строки (~+11…+26 к HEAD: +> `renderEditorConstraintBlock` :515 не :489, `suppressContained` :684, `dispositionFor` :441; санитайзер = v6 +> не v4). Этот план цитирует HEAD-строки. + +--- + +## §0. Рамка: доказанная база и зоны + +**Строим (ратифицировано D39.x; в план внесено, НЕ пере-открывается):** волновой раннер W0→W1(∥)→W1.5→W2(∥); +бюджет чанкера в ВЫХОДНЫХ токенах (фертильность 1.20/0.39); банк W1.5 слоя 4 (детектор V-C WHICH + банкнота dst + +Палладий + консолидация §C2 + алиас-ярус-1 + карта подписи); декаплинг draft=мелкий-чанк / edit=крупная-единица; +свап редактора glm→mistral/deepseek-pro; дефект-класс чекеры + данные пакета пары. + +**НЕ строим (доказано/ратифицировано):** сцен-детекция и carryover-машинерия (D39.7/8, метрики под полом 0.126 И +читатель); DP/logical-граница чанкера (Q1: «граница не рычаг»; Q1a 0/7); ко-оккуренция для dst (canon-recovery +0.20–0.68 < 70%); LLM-сегментация/ML-сцены/эмбеддинг-границы/RAG-оверлап; фронтир в дефолт; жанр-паки; локальный +9B-споттер (код ≥ локаль). **Парковки (не тестировано, консервативный дефолт — НЕ строим, НЕ хоронить):** +lookahead (Q3b), diff-редактор §C2 (Q4b), reflow-отдельным-пассом (Q4c), широкое-окно/узкая-эмиссия (Q2c), +W2.5-швейный пасс (Q5). Каждая парковка помечена «не тестировано» — диспозиции в §9 (lookahead/diff-редактор через +§1(е)/E.7; Q4c/Q2c/Q5 — явные строки в §9). + +**Столпы (инварианты 1–8 `backend/README.md`, ломать нельзя):** деньги/durability (committed==SUM, kill-9); +снапшот-дисциплина (правка wire/вердиктов = громкий resnapshot; все правки этого плана → ЕДИНЫЙ resnapshot §8); +детерминированный банк (не вектора); эхо-мина DeepSeek (thinking НИКОГДА не off); 18+; конфиг-первое ядро; +экспорт-контракт (внешняя экстракция ТОЛЬКО `tmctl export`); golden-гард детерминизма; инвариант общности §0.1 +(любая книга/пара/структура; пара = ось данных). + +**Сводка $0-верификаций (все PASS, детали в каждом WS (г)):** + +| WS | Верификация | Результат | +|---|---|---| +| WS2 | фертильность независимо пере-выведена | `1.1978/0.3852, R²=0.9633` (Δ<3e-5); классиф.-mismatch на zh = 0 | +| WS2 | нарезка на 25 главах | target_out=1797 → тот же СЧЁТ 56 чанков (4 ре-сегм / 2 абзац-сдвига, НЕ byte-identical); edit-единица: 8000-арм 25/25 в 2k–8k, **ратиф. дефолт 3200 → 37 единиц** (11 глав делятся; размер ГЕЙТИТСЯ Q2a) | +| WS3 | майнер-v1 golden | катастроф `{方源:0,蛊:1,蛊师:2,古月:21}` EXACT (вариант A / Python-референс; при ратиф. дефолте **B** → `古月:22`, скрин PASS — §3(г)/(д) split-фикстура); 13618 канд.; recall 0.965/0.932 | +| WS3 | сид-дельта + линт | 61 терм, 0 фейл-лаудов; флагает D38-дефект как draft ⟨проверить⟩ | +| WS4 | банкнота пере-парс | 95 строк/65 src/0 fail/0 trunc EXACT; hash resume-стабилен | +| WS5 | чекеры | 6/6 позитивов; DC1/2/6 n=1 (реальные, FP-rate §5д); DC3 3.5% (1 паратекст + 1 narrative-coref на hidden); **DC5 74%/42% → не гейт**; DC7 не покрыт $0 | +| WS1 | деньги под ∥ + wall-clock | money-инвариант ALL PASS (границы ПАРАЛЛЕЛЬНЫ); within-wave compute ≈7.3× (end-to-end + W1.5-стоп) | +| WS6 | COGS per-арм | glm-5 $0.335 / mistral $0.205 / deepseek-pro $0.117 (25-гл, warm/valley) | + +--- + +## §1. WS1 — Волновой раннер (слой 1 / оркестровка) + +Целевой раннер: **W0** (чанкер + контекст-пакеты, $0) → **W1** черновики параллельно → **W1.5** (стоп-граница: +банк-синк, подпись владельца, resnapshot) → **W2** редактуры параллельно. Сегодня перевод строго последователен: +`bookrun.go` итерирует плоский список чанков, `translateChunk` (`chunkrun.go:90`) гонит ВСЕ стадии одного чанка по +порядку под ОДНИМ `snapID`. Волны разносят: W1 = только draft-стадия по всем чанкам; W2 = только edit-стадия по всем +единицам; W1.5 между ними. + +### (а) Что меняем (модули, file:line) +- **`runner.go`** — новый волновой драйвер (или `bookrun.go` расширяется режимом waves); **закрыть гонку lazy-init + `Runner.client` (`runner.go:171-181`)**: `r.clients map[string]llm.LLMClient` читается+пишется без лока. +- **`bookrun.go`** — цикл книги переходит на волны; per-chapter sticky-reset (`stickyWin=nil`, HEAD ~`bookrun.go:144`) + переезжает в W0-precompute. +- **`chunkrun.go`** — `translateChunk` расщепляется на per-волна проходы; sticky-цепочка (`chunkrun.go:107-115`: + `memSel=r.memory.Select(...)` → `activeIDs` → следующий stickyPrev) precompute'ится в W0. +- **`snapshot.go`** — `snapshotID()` (`:101-279`) становится **пер-волновым** (см. (в)). +- **`stagerun.go`** — деньги/чекпоинт-путь (`Reserve`→`SettleWithCheckpoint`, `:312-449`) без изменений семантики, + но вызывается из N горутин; `client(model)` предвычислен. +- **`config/pipeline.go`** — новый конфиг `waves.workers` (число воркеров волны — НЕ `fanout.workers`: `Fanout` + занят C2-кандидатами, `CheckRunnable` отвергает `candidates>1` `pipeline.go:208-209`; оси ортогональны) + + `pricing_windows` провайдера + **пер-модельный rate-guard** (ревью-2 F4): `models.yaml` `max_concurrency`/опц. + `min_interval` НА МОДЕЛЬ (конфиг, НЕ хардкод); rate-guard = транспорт-ось, НЕ снапшот (wire-байты не трогает). + +### (б) Алгоритм (детерминизм явно) + +**W0 (детерминированный, $0):** +1. `SplitChunks` (draft-чанки, WS2) + сборка edit-единиц (WS2). +2. **Predвычислить sticky-цепочку:** один упорядоченный проход по чанкам КАЖДОЙ главы; для чанка `i` + `stickyPrev_i = union предыдущих stickyDepth=2 чанков той же главы` (порт `bookrun.go` unionSticky), затем + `memSel_i = memory.Select(chunk_i, chapter, stickyPrev_i, budget)`, `activeIDs_i` кормит `stickyPrev_{i+1}`. + `Select` — ПУРЕ детерминированная функция `(chunk, chapter, stickyPrev, budget, frozen-bank)` (`memory.go:324`), + src-производна, БЕЗ модель-выхода → precompute воспроизводит БАЙТ-в-БАЙТ ту же инъекцию, что онлайн-цепочка + последовательного раннера. **Инвариант: precompute'нутые инъекц-байты ≡ байтам текущего раннера** (golden-тест в + (д)) → sticky-переезд НЕ двигает request_hash, НЕ строка манифеста resnapshot. ⚠ Byte-identity держит только над + BASE-банком и над НОВОЙ нарезкой (WS2 сдвигает 4 чанка); golden-тест (д) сверяет против нового чанк-сета, не старого. + (Параллелизм байты НЕ меняет — `Select` src-производна, порядок фиксирован; ревью-1 CONFIRMED.) +3. **Eager-build клиентов:** множество моделей известно из `Pipeline.Stages[].Model` ∪ `EscalateTo` до старта → + построить ВСЕ `llm.LLMClient` в W0 (`BuildClient`, `clients.go:15`), заполнить `r.clients` ДО любой горутины. + Закрывает гонку `runner.go:171-181` без лока (карта только читается в волнах; `r.client()` вызывается ровно в одном + месте `stagerun.go:343` с `model∈{st.Model, EscalateTo}` — перечисление ПОЛНОЕ). **Защита от будущей 3-й модель-оси** + (channel B / annotator, `stagerun.go:476-478`): в волновом режиме `client()` делается READ-ONLY (panic/error на miss), + чтобы не-перечисленная модель не пере-открыла гонку тихо. Прочие разделяемые хендлы: + `templates` (построена раз в `loadTemplates`, read-only в волнах — safe), `memory *MemoryBank` (материализован + раз, immutable — safe), `Store` (single-writer сериализует записи — safe), `Log` (slog thread-safe). + +**W1 (∥):** N воркеров тянут draft-чанки из очереди; каждый — `runStage(draft-стадия)` под `snapshot_W1`. Деньги: +`Reserve`→`Complete`→`SettleWithCheckpoint` (`stagerun.go`) — сериализуются через single-writer store-пул +(`store.go:71` `MaxOpenConns=1`, BEGIN IMMEDIATE). Ретрай чанка `i` изолирован: его вход — чистая функция +`(source_i, frozen-bank, snapshot_W1)`, независим от чанка `j` (свойство волн — нет разделяемого мутабельного входа). + +**W1.5 (стоп-граница, $0 к провайдерам кроме опц. облачной Z1-адъюдикации):** банк-майнинг из W1-черновиков +(WS3) → алиас-кластеризация ярус-1 → канон-предложения (draft) → карта подписи (сайдкар) → **подпись владельца** +(approve/правка/draft/расклеить) → эмиссия сид-дельты → reseed → `snapshot_W2`. Механика стопа (см. (в)). + +**W2 (∥):** N воркеров тянут edit-единицы (главы, WS2); каждый — `runStage(edit-стадия)` под `snapshot_W2`, читая +draft из W1-чекпоинта (`prev` = draft-текст единицы). W1-чекпоинты под `snapshot_W1` НИКОГДА не пере-гоняются → +W1 не пере-оплачивается. **Инъекция редактора W2 (ревью-2 F3, специфицировано):** edit-стадия единицы получает +СВЕЖИЙ `memory.Select` по тексту ВСЕЙ единицы (глава) над ОБОГАЩЁННЫМ банком (sticky-окно на уровне глав вырождено: +единица = глава); констрейнт-блок `renderEditorConstraintBlock` = CONFIRMED-only, формат src→dst (решение WS2 §2(б)). +**Явный запрет: НЕ реюзить per-чанк `memSel` из W0** (`chunkrun.go:107-114`) — тот посчитан над BASE-банком, W2 поехала +бы БЕЗ майненого банк-энфорса. Это НЕ противоречит инварианту секвенирования (в): свежий Select кормит ТОЛЬКО +edit-стадию (CONFIRMED-only enriched-банк), draft-стадия по-прежнему НЕ пере-рендерится/не пере-хешируется. Пин — (д): +инъекция W2 содержит подписанные на W1.5 mined-термы. + +**Пер-модельный rate-guard (ревью-2 F4, НЕСУЩЕЕ для ∥):** пер-ВЫЗОВНЫЙ 429/`Retry-After` УЖЕ есть в адаптере +(`httpllm.go:27-29,138-140`, читается+уважается, кап `maxRetryAfterWait=5мин` `:76`; тест +`TestRetryOn429HonoursRetryAfter` `httpllm_test.go:168`) — до WS1 раннер БЫЛ последователен, проблема +N-параллельности НЕ возникала. Гэп именно N-конкуренция. Волновой диспетчер добавляет **пер-модельный семафор +конкуренции** (+опц. пейсинг `min_interval`) из `models.yaml` + телеметрию 429-плотности. Rate-guard **НЕ снапшот-ось** +(транспорт, wire-байты не трогает). ⚠ Известный лимитер: `mistral-large-latest` ~48% retry-fails @1.3s интервала, +max lat 64.7s (судейский eval-риг exp15 §7.6 REV.2; grok 0%); расширение 1.3→2.6s эффекта НЕ дало (49.5→48.2%) → +похоже на токен-бакет/конкуренцию, тир-зависимо → **mistral-арм НЕ идёт через прод-путь до rate-guard** (WS6 +пред-условие). Заготовка-блок для `00-provider-quirks.md` — §12 (вносит оркестратор при лендинге, зона полигона). + +**Money под параллелизмом (доказано верификацией):** потолок проверяется ВНУТРИ `Reserve` immediate-транзакции +(`ledger.go:56-72`: `committed+reserved+estimate ≤ ceiling`) → конкурентные резервы N воркеров сериализуются +single-writer'ом, overshoot ≤ одной макс-резервации. Урок D12 (гонка `Runner.clients` + commit-terms-on-boundary): +клиенты предвычислены (закрыто); commit-терминов на границе джобы в этой архитектуре не применяется (банк +консолидируется в W1.5, не на границе чанка). + +### (в) Конфиг + снапшот-механика — **ПЕР-ВОЛНОВОЙ снапшот (несущее)** + +Сегодня `snapshotID()` — book-global, фолдится в КАЖДЫЙ `request_hash` (`stagerun.go:268-269`: `snapID` — прямой +вход). Правка `snapID` = all-or-nothing re-bill (`snapshot.go:26-34`); content-addressed reuse (`msgsContentHash`) +гейтится НЕ-изменённым снапшотом → через resnapshot не срабатывает (D15.2 — не построен). **Проблема:** банк входит +в `snapshotID` через `MemoryVersion` (`snapshot.go:191,229`); наивный resnapshot после W1.5 сдвинул бы `snapID` для +ВСЕХ стадий, включая W1 → пере-оплата W1 → «две переоплаты». + +**⚠ Коррекция после адверсариал-ревью-1 (несущее): «переоплата ОДНА» держит НЕ снапшот, а ВОЛНОВОЙ ИНВАРИАНТ +СЕКВЕНИРОВАНИЯ.** `request_hash` фолдит рендер `msgs` НАПРЯМУЮ (`stagerun.go:268-269`), не только через `snapID`; +resume-fast-path отдельно сверяет `cs.ContentHash == msgsContentHash(msgs)` (`stagerun.go:65,79`). W1-инъекция +переводчика (`renderGlossaryBlock`, `memory.go:480,484-487`) эмитит ВСЕ строки с непустым dst, ВКЛЮЧАЯ AMBIGUOUS +(auto/draft, помечены `⟨проверить⟩`) — а WS3-майнинг добавляет до 21 draft-строки с dst. ⇒ если W2-проход когда-либо +ПЕРЕ-рендерит/пере-хеширует draft-стадию над обогащённым банком, её `msgs→contentHash→request_hash` меняются → miss → +re-bill W1, даже при byte-identical `snapshot_W1`. **Несущий инвариант: W2-проход НИКОГДА не трогает рендер/хеш +draft-стадии; редактор получает черновик из W1-ЧЕКПОИНТА (`final_hash→response_text`), не из свежего draft-рендера над +обогащённым банком.** Это ПРЯМО противоречит текущему `translateChunk`, который считает `memSel` ОДИН раз и рендерит +инъекции ОБЕИХ стадий из него (`chunkrun.go:107-114`) — наивный волновой сплит, пере-считывающий `memSel` в W2 для +content-verification, пере-выведет draft-инъекцию над обогащённым банком и споткнётся ровно тут. Волновой раннер обязан +разнести: W2-проход рендерит ТОЛЬКО edit-стадию, черновик читает по `final_hash`. + +**Роль пер-волнового снапшота (уточнено):** он НЕ предотвращает re-bill (это делает инвариант выше) — он **избегает +громкого resnapshot-СТОПА** (`stagerun.go:45-49`): на резюме-после-W1.5, если владелец промоутнул майненые строки в +approved (это ДВИГАЕТ `MemoryVersion`, `memory.go:264-282`), book-global снапшот сделал бы `snapshot_id` уже-готовых +draft-джоб ≠ пере-вычисленному глобальному → hard-стоп. Пер-волновой снапшот держит W1-джобы на `snapshot_W1` (стабилен) +→ стопа нет. `snapshotID` вычисляется **пер-волна**, фолдя ТОЛЬКО стадии этой волны + состояние банка на момент волны. +Хранение уже пер-стадийное — `jobs` UNIQUE(book_id, chapter, stage) (`migrate.go:37`), каждая джоба несёт свой +`snapshot_id`. **Под-специфицировано (реальная ре-архитектура, не кноб — бэкенду построить):** сегодня `snapshotID()` +фолдит ВСЕ стадии + ОДИН book-global `MemoryVersion` в ОДИН хеш (`snapshot.go:229`), `TranslateBook` апсертит РОВНО один +снапшот (`bookrun.go:117-123`). План требует: раннер вычисляет ДВА `MemoryVersion` (base vs enriched, разбивка по +`Source` ниже), апсертит ДВА снапшота (W1/W2), пинит стадии волны к своему. Сейчас все стадии получают ОДИН `snapID`; +план делает `MemoryVersion` (и stage-plan) компонентом волны: +- `snapshot_W1 = f(brief, chunkerVersion, draft-stage-plan, base-bank-version, coverage/sanitizer/classifier/…)`. +- `snapshot_W2 = f(brief, chunkerVersion, edit-stage-plan, enriched-bank-version, …)`. +W1.5-обогащение банка двигает ТОЛЬКО `enriched-bank-version` → ТОЛЬКО `snapshot_W2`. W1 стабилен. + +**Механизм стабильности W1 на РЕЗЮМЕ (несущее, закрывает re-bill-дыру):** после W1.5 сид ОБОГАЩЁН (дельта влита) → +наивный пересчёт `snapshot_W1` из ТЕКУЩЕГО (обогащённого) банка дал бы enriched-memory-version → mismatch с замороженным +в W1-джобах `snapshot_id` → resnapshot-стоп → re-bill W1. Фикс использует поле **`Source`** (`glossary.go:36`, +`seed|ruby|auto` + новое `mined` WS3): **`base-bank-version` = хеш approved-строк с `Source∈{seed,ruby}` (БЕЗ mined); +`enriched-bank-version` = хеш ВСЕХ approved-строк (вкл. `Source:mined`).** Майненые строки исключены из W1-фолда → +`computeMemoryVersion`-вариант для W1 пере-вычисляется из НЕ-майненых строк, стабилен через W1.5-обогащение (майнинг +только ДОБАВЛЯЕТ mined-строки, не трогает seed/ruby) → `snapshot_W1` воспроизводится идентично на резюме → W1-чекпоинты +ХИТ, не пере-оплачиваются. Детерминировано (фильтр по `Source` + сортировка `GlossaryForBook`). Пиновый тест — (д). + +**⚠ Скоуп подписи W1.5 = ТОЛЬКО `Source:mined` (ревью-2 F2, инвариант — прямое следствие фикса выше):** mid-run +подпись владельца на W1.5-границе трогает ИСКЛЮЧИТЕЛЬНО `Source:mined`-строки (промоут draft→approved, правка dst +майненого терма). Правка/промоут **APPROVED** `Source∈{seed,ruby}`-строки ДВИГАЕТ `base-bank-version` → mismatch с +замороженным в W1-джобах `snapshot_W1` → resnapshot-СТОП / re-bill W1 — поэтому разрешена ТОЛЬКО как ОТДЕЛЬНОЕ +resnapshot-событие ВНЕ прогона (пред-прогонный reseed §8), НЕ на W1.5-границе. (Уточнение precision: DRAFT-status +seed/ruby-строка НЕ фолдится в `base-bank-version` [gate off, `memory.go:264-266`], но W1-инъектится через +`renderGlossaryBlock` → её правка = ТИХИЙ per-чанк re-bill через contentHash-путь `stagerun.go:72-79`, не громкий стоп; +реалистично seed/ruby = approved, пин (д) это разделяет.) +- **Write-path `Source:mined` (ревью-2 пост-верифай, НЕСУЩЕЕ — закрывает грунт-гэп критика):** `loadGlossarySeed` + БЕЗУСЛОВНО штампует `Source:"seed"` (`memseed.go:131`) — наивный reseed W1.5-дельты через него проштамповал бы + майненые термы `Source:seed` → они попали бы в `base-bank-version` → сдвинули `snapshot_W1` → сломали «переоплата + ОДНА». ⇒ **персист майненой дельты после подписи идёт ОТДЕЛЬНЫМ mined-write-путём** (штамп `Source:mined`, зеркало + `rubyToCandidates`'s `Source:"ruby"` `memseed.go:352`) — НЕ через немодифицированный `loadGlossarySeed` (хардкодит + `Source:"seed"` `:131`) и НЕ расширением `seedTerm`-схемы (та остаётся БЕЗ новых полей — §3(б); Source-класс живёт в + сайдкар-карте подписи). Пин — WS3 (д). +- **Промоут `甲乙丙丁`/`四代族长` (DC-7):** майнер СЮРФЕЙСИТ их как `Source:mined` draft (флаг-для-видимости §3(б)); но их + промоут в approved-канон = **ЭЛЕВАЦИЯ в `Source:seed`** (канон грейд/титул = base-seed-класс, не run-time mined) → + поэтому строго в пред-прогонный reseed (§8, `MemoryVersion` двигается), НЕ в W1.5-подпись. +Пин-тест — (д): правка approved seed-строки после W1 → ГРОМКИЙ resnapshot-стоп, не тихий re-bill. + +**Механика W1.5 для ОБОИХ состояний гейта (доказуемо «переоплата ОДНА»):** +- **`PostcheckGate=false` (текущий дефолт):** `computeMemoryVersion(rows, gateOn=false)` фолдит ТОЛЬКО approved-строки + (`memory.go:264-266`). Майненые auto/draft НЕ двигают снапшот; их вливание меняет лишь инъекцию стадий, которые их + инъектят. На момент W1 банк = base (майнинг идёт ПОСЛЕ W1) → W1-инъекция = base. В W1.5 подпись владельца + промоутит часть в approved → `enriched-bank-version` меняется → `snapshot_W2` двигается ОДИН раз. Auto/draft + (AMBIGUOUS) в редакторский блок (`renderEditorConstraintBlock`, CONFIRMED-only, `memory.go:515`) НЕ входят. +- **`PostcheckGate=true` (hard-gate, висит за пере-замером D24.4/D28.1):** `computeMemoryVersion(rows, gateOn=true)` + фолдит ВСЕ строки вкл. auto/draft + `gate:`-флаг (`memory.go:251-266`, `snapshot.go:86-93`). Но на момент W1 + майненых строк ещё НЕТ → `base-bank-version` реально base. В W2 банк обогащён → `enriched-bank-version` включает + все майненые строки. Пер-волновой снапшот держит «одну переоплату» и здесь: W1 под base, W2 под enriched. + +**UX-решение (стоп посреди книги) — владельцу** (§10 вопрос W1.5-UX; §E-5 research/20). Дефолт плана: W1.5 — **явная +стоп-граница** (не mid-run-append): раннер завершает W1, пишет карту подписи, ОСТАНАВЛИВАЕТСЯ; владелец подписывает; +`tmctl` возобновляет с W2 под `snapshot_W2` (`--resnapshot` принимает сдвиг). Sticky/scene-инерция precompute'ится в +W0 (src-производна) — не ломается параллелизмом. + +**Scheduler-aware диспетчер:** ценовые окна = **конфиг провайдера** (`providers.json`/`models.yaml`), не хардкод. +Вендор-чеки ($0, разрешены): +- **(а) DeepSeek пик/офф-пик — РАЗРЕШЁН (флаг D39.10-6):** два механизма из разных эр, НЕ конфликт. Легаси off-peak + СКИДКА `16:30–00:30 UTC −50%/−75%` — только **V3/R1**, НЕ переносить на v4. Актуально для v4: **PEAK-надбавка ×2** в + **UTC 01:00–04:00 ∪ 06:00–10:00** (пики Пекин 09–12 & 14–18); офф-пик = baseline (доп. скидки НЕТ). + ⚠ **×2 подтверждён ТОЛЬКО вторичкой** (TechNode/explainx/KuCoin); офиц. `api-docs.deepseek.com/quick_start/pricing` + на 2026-07-19 — ФЛЭТ, без peak/off-peak. Правило двух направлений: **держать ×2 как РИСК-ПИН**, ре-чек у катовера + 24.07. Раннер: платные DeepSeek-джобы вне пик-окон; нельзя — закладывать ×2, не блокировать. (URL: docs §157–167.) +- **(б) ZAI/GLM префикс-кэш — РАЗРЕШЁН (§E.8-бис):** implicit префикс-кэш подтверждён на `glm-5` + (`docs.z.ai/guides/capabilities/cache`); cache-hit = **$0.20/1M (−80% от input $1.00)**, НЕ 50% из иллюстрации доки; + хранение бесплатно (limited-time); TTL официально НЕ документирован (агрегаторные «5мин/512ток» НЕ подтверждены). + Warm-then-fan оправдан: warming-вызов платит полный input, фан-аут-соседи — cache-hit. Инвариант байт-идентичного + ПРЕФИКСА (`[система+мандаты]+[глоссарий сорт-ключи]→[волатильный чанк в хвост]`, research/19:506): любой timestamp/ + счётчик-ретрая/«почему упало» — только в ХВОСТ (`:213`). Бэкенд УЖЕ читает `cached_tokens` + (`httpllm.go:232-255`); реальную долю cache-hit верифицировать в ledger (эмпирика бьёт веру, TTL-риск). +- **(в) Слаги DeepSeek 24.07 — РАЗРЕШЁН:** `deepseek-chat`/`deepseek-reasoner` депрекация **2026-07-24 15:59 UTC** → + оба на v4-flash (chat=non-thinking, reasoner=thinking). Наш прод `deepseek-v4-flash`/`deepseek-v4-pro` — НЕ задет. + **Действие бэкенду:** grep `configs/models.yaml` + eval-скриптов на legacy-слаги + live `GET /models`. (research/19 + §E.8: eval-скрипты несут legacy-слаги — `exp15/preflight_models.py:32`, `local_bench.py`, `editor_bench.py:36` — + это eval-долг, не прод.) + +### (г) Верификация ДО стройки ($0) — `ws1_wave_cogs.py`, РЕЗУЛЬТАТ +- **Money-инвариант (mock, моделирует single-writer admission-ось `ledger.go`; НЕ полное зеркало — см. каветы): + ALL PASS** — `committed==SUM($0.448)`, `reserved=0`, **kill-9 без двойной оплаты** (идемпотентный settle + `ledger.go:143` ON CONFLICT DO NOTHING). ⚠ **Границы — ПАРАЛЛЕЛЬНЫЕ, не последовательные** (адверсариал-ревью-1): + (1) kill-9 теряет **≤N in-flight вызовов** (`recoverReservations` обнуляет ВСЕ `reserved_usd`, `store.go:219`), + каждый пере-оплачен ОДИН раз — не «≤1»; (2) overshoot: реальный `Reserve` ОТКЛОНЯЕТ при `bookTotal+estimate>ceiling` + (`ledger.go:67-71`) → reserved потолок не пробивает; единственный overshoot-источник = settle бронирует `cost` при + `cost>estimate` (pricier `modelActual` `PriceForResponse` `stagerun.go:420`; reasoning-overflow) → под N резервами + bound = `Σ(cost_i−est_i)`. Mock это НЕ проверяет (`cost=0.008estimate` (pricier modelActual): overshoot ≤ Σ shortfall. +- Golden: **инъекц-байты W0-precompute sticky ≡ байтам текущего последовательного раннера** над НОВЫМ чанк-сетом WS2 + (fresh↔resume; sticky не двигает request_hash над base-банком). +- **НЕСУЩИЙ пин (ревью-1 F1): draft `request_hash`/`contentHash` БАЙТ-СТАБИЛЕН под ОБОГАЩЁННЫМ банком** — W2-проход не + пере-рендерит draft-стадию; редактор читает черновик по `final_hash`. (Не просто «`snapshot_W2` двигается».) +- Пер-волновой снапшот: промоут майненых→approved двигает ТОЛЬКО `snapshot_W2` (W1-джобы не ловят resnapshot-стоп); + `base-bank-version`=хеш `Source∈{seed,ruby}`-строк стабилен через W1.5. +- **Пин скоупа подписи W1.5 (ревью-2 F2):** правка/промоут `Source∈{seed,ruby}`-строки ПОСЛЕ W1 → ГРОМКИЙ + resnapshot-стоп (не тихий re-bill W1); правка ТОЛЬКО `Source:mined`-строки → двигает лишь `snapshot_W2`, W1 стабилен. +- Race-тест `Runner.clients` (D12-урок): `client()` read-only в волнах, panic на miss не-перечисленной модели. + +### (е) Открытое / маршрутизация +- **Соседи-черновики как контекст W2 — НЕ ратифицированы** (Q3a null + гипотеза CJK-протечки V2-брака §7.9: src- + carryover → больше исходника → выше протечка). **Дефолт: без них.** Если бэкенд захочет — обоснуй + дешёвый гейт. +- **Lookahead (Q3b) — парковка** (не тестировано). Не в дефолт. +- **W1.5-UX (стоп посреди книги vs автопродолжение)** — вопрос владельцу (§10). +- Пик-надбавка DeepSeek ×2 — риск-пин, ре-чек 24.07 (§10). + +--- + +## §2. WS2 — Чанкер-бюджет + крупная edit-единица (слой 1) + +### (а) Что меняем (file:line) +- **`chunker.go`** — бюджет из `const targetChunkTokens = 1500` (`:47`, единица `estTokensFrom=cjk+other/3` `:173-181`) + → **конфиг per-pair в ВЫХОДНЫХ токенах через фертильность**; `Chunk{Chapter,ChunkIdx,Text}` (`:32-36`) расширяется + аддитивно (`EstOut float64`, `OversizedSentence bool`, `EditUnitID int`). Мега-ПРЕДЛОЖЕНИЕ (`packSentences:129-152`: + своё оверсайз-под-чанком БЕЗ клауза-сплита и БЕЗ флага) — решить (см. (б)). +- **`config/pipeline.go`** — снести мёртвые `STMDepth`/`OverlapTokens` (`:47-48`, carryover не строим); добавить + `Segmentation{DraftBudgetOut int, EditCeilingOut int, Fertility{CJK,Other float64}}` (per-pair, снапшот-folded). +- **`coverage.go`** — инвариант floor: draft-бюджет держит чанки над `Gates.Coverage.MinChunkChars` (`pipeline.go:159`, + дефолт 500); связь-инвариант, не жёсткий (хвосты глав легально < floor — документ. граница `chunker.go:42-45`). +- **`snapshot.go`** — `contextSnap` (`:15-21`) теряет STMDepth/OverlapTokens; добавляется `segmentationSnap` + (бюджет+фертильность+edit-ceiling, фолд как `coverageSnap`). +- **новый `slojpjat`-омиссия-бэкстоп** в слое 5 (см. WS5/(б)) — компенсатор зоны деградации. + +### (б) Алгоритм +- **Draft-чанк = мелкая единица (COGS/coverage/выравнивание):** greedy-упаковка абзацев (как сейчас, + `appendChapterChunks`), но бюджет = `est_out(chunk) = 1.1978·cjk + 0.3852·other` (ВЫХОДНЫЕ токены) вместо + input-est. Оверсайз-абзац → `packSentences`; НИКОГДА не резать предложение. (WS2 (г): на zh смена единицы + near-нейтральна — тот же СЧЁТ 56 чанков при target_out=1797, НО **4 из 56 чанков ре-сегментируются (2 реальных + абзац-сдвига** на границе накопления, где est_out other-вес 0.3852 vs input-est integer other/3 расходятся) → НЕ + byte-identical → законный `chunkerVersion` bump §8. Ни один target_out не даёт 0 diff.) `strategy`-ось (`greedy|logical`) — + заложить как СЕАМ (аддитивное поле), но наполнение `logical` НЕ строить (Q1 «граница не рычаг», Q1a 0/7). DP §B1.5 + (штрафы B0:0/B1:1/B2:3/B3:6/B4:10, веса w_size=3/w_bound=60/w_dialog=200) — справочно в research/19, НЕ реализуем. +- **Мега-предложение (решение плана):** `packSentences` пропускает мега-ПРЕДЛОЖЕНИЕ оверсайз-под-чанком без флага. + **Решение: passthrough + флаг `OversizedSentence=true`** (не аварийный клауза-сплит ;:,、 из §B1.4). Обоснование: + клауза-сплит = граничная машинерия (анти-скоуп); «деградация не молчит» удовлетворяется ФЛАГОМ (наблюдаемость). + Мега-абзац уже покрыт (`packSentences`). Стихи/паратекст — как обычный текст (§0.1: не спец-кейсить структуру + книги); стих-РЕНДЕР — пак-политика WS5-DC5. +- **Edit-единица = глава** — ДЕКАПЛИНГ draft/edit ратифицирован (D39 п.4, exp14 претензия-1: редактору нужна КРУПНАЯ + единица для меж-чанковой когезии). Глава = естественная дискурс-граница; глава > `EditCeilingOut` → деление по + АБЗАЦНЫМ границам greedy до потолка (never-split-paragraph). На 25-гл книге при **дефолте 3200: 11 глав делятся → + 37 edit-единиц** (при гейтнутом 8000-арме — 25 цельных глав, 0 > 8000, max 6528, все в 2k–8k; ревью-2 F1 — детали + (г)). ⚠ **РАЗМЕР единицы ГЕЙТИТСЯ (ревью-1 F1, не «безопасно»):** span-омиссия Q2a НЕ + гонялась; omission-бэкстоп WS5 — сеть ENTITY-уровня (сущность есть где-угодно), СТРУКТУРНО СЛЕП к пропуску + клауза/сцены внутри 6.5k-единицы, чьи сущности повторяются (сам `reflow_omission.py:205-212`: «NOT evidence big + chunks omission-safe… needs SPAN-JUDGES; Q2b cannot ratify edit=chapter, only Q2a»). **Решение:** глава-единица — + ЦЕЛЬ; для пере-прогона `EditCeilingOut` — **консервативный дефолт ≤3200 out** (край нижней зоны деградации, а не + 6.5k), большая-глава-арм ГЕЙТИТСЯ платными Q2a span-судьями (прогон-протокол WS6/(д)) + чтением владельца (2-претензии + ловят span-омиссию). Owner-развилка §10. Единица маппится на чекпоинты: **деньги/resume per-СТАДИЯ-волна**, НЕ + per-микро-чанк — против инварианта №1: чекпоинт = единица оплаты стадии, волна = последовательность. +- **Сайзинг edit-единицы (ревью-2 F8, ИСПРАВЛЕНО пост-верифай):** unit-awareness сайзинга УЖЕ автоматическая — + `stagerun.go:87-90` (D2.5) сайзит `max_tokens` от `sizingText=prev` (для стадии >0 = ЧЕРНОВИК всей единицы, НЕ + источник): `EstimateTokens(prev)·MaxOutputRatio` + пер-модельный флор D24.3 (`applyModelFloor`); `Reserve` тоже + text-proportional по msgs. ⚠ **НЕ сайзить от `est_out` единицы** (первичная формулировка F8 «от est_out» НЕВЕРНА): + `est_out=1.20·cjk+0.39·other` ИСТОЧНИКО-производна и в glm-4.6 output-token-пространстве (≠ эвристика `EstimateTokens` + cjk+other/3), а D2.5 прямо сайзит редактора от ЧЕРНОВИКА (~1.9× источника) — сайзинг от источника недо-бюджетит и + ложно-триггерит length-ретрай. ⇒ единственное требование: edit-единица передаёт СВОЙ ПОЛНЫЙ черновик как `prev` + (волновой дизайн W2 это и делает, §1(б)); НОВОЙ формулы сайзинга НЕТ → строки манифеста §8 F8 не требует. **Инъекция + единицы (ревью-2 F3, детали §1(б)):** свежий `memory.Select` по тексту ВСЕЙ единицы над ОБОГАЩЁННЫМ банком (НЕ реюз + per-чанк W0-`memSel` над base); формат src→dst, CONFIRMED-only. +- **Формат банк-констрейнтов редактора (открытый вопрос D30.1, `memory.go:505-511` — решается ЗДЕСЬ, парковка + запрещена):** сегодня `renderEditorConstraintBlock` (`:515`) даёт ГОЛЫЕ dst-формы (`- «dst»`, CONFIRMED-only, + дедуп по dst). **Решение: перейти на src→dst-мэппинг** (`- 源термин → «dst»`), как у переводчика. Обоснование + (единственное несущее — ревью-1 F4 снял два ложных): **редактор БИЛИНГВ (D30.1, `editor.md:1` видит источник) — + привязка канона к src-терму устраняет неоднозначность при ОМОНИМИЧНЫХ dst** (одна ru-поверхность на две сущности). + ⚠ Отвергнутые обоснования: «AMBIGUOUS-строк станет больше» — НЕ-секвитур (блок CONFIRMED-only `memory.go:519`, майненые + AMBIGUOUS сюда НЕ входят); «инвариант кэш-префикса» — нет (блок из per-chunk `memSel.injected` = волатильный ХВОСТ, не + стабильный префикс). Мех-безопасность: `RequestHash` хеширует `msgs` вкл. блок → формат-правка меняет request_hash, + тихого false-hit нет; CONFIRMED-only СОХРАНЯЕТСЯ; дедуп по (src,dst) лишь РАСШИРЯЕТ список (безвредно). **Снапшот-фолд + ЯВНЫЙ:** формат рендера `renderEditorConstraintBlock` НЕ схватывается `memoryMatchVersion` (тот = нормализатор/матчер) — + добавить версию формата рендера как folded-компонент снапшота (иначе loudness держится дисциплиной, не механизмом). + +### (в) Конфиг + снапшот +- Фертильность `{cjk:1.1978, other:0.3852}` — конфиг слоя 2 (per-pair), снапшот-folded; **пересчёт = громкий + resnapshot** (D30.9). Go грузит ТОЛЬКО коэффициенты (не токенайзер). +- `DraftBudgetOut` дефолт **1797** (тот же СЧЁТ 56 чанков, что текущий input-est=1500 на zh; 4 чанка ре-сегментируются, + 2 абзац-сдвига — НЕ byte-identical, WS2 (г)) — «tunable, ревизия по пере-прогону». +- `EditCeilingOut` дефолт **3200 out** (ревью-2 F1: приведено к WS2(б)/§7/§10-13/§11 — прежнее «8000» ЗДЕСЬ + противоречило консервативному дефолту; 3200 = край нижней зоны деградации) — tunable. **8000 out = потолок + ГЕЙТНУТОГО крупно-главного арма** (верх зоны деградации research/19 §A.1.1), открывается ТОЛЬКО по платным Q2a + span-судьям (§10-13), НЕ дефолт. +- Снос STMDepth/OverlapTokens меняет `contextSnap`-структуру → **строка манифеста resnapshot §8** (снапшот двигается). +- ⚠ **Классификатор CJK (WS2 (г) находка §1.6):** `fertility_calib.is_cjk` = ord-диапазоны БЕЗ Hangul; + `EstimateTokens` (`render.go`) = `\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}`. На ЭТОМ zh-корпусе — **0 сдвиг + коэффициентов** (verified — но КОРПУС-СПЕЦИФИЧНО: корпус не содержит спорных символов `々/〇`/CJK-радикалов, kana-марок + `・ー`). ⚠ **ws2_fertility_verify классификатор B — НЕ верный Go-прокси** (ревью-1 F3: исключает `々/〇`/радикалы, + что `unicode.Han` включает; включает `・ー`, что `unicode.Katakana` исключает) → скрипт НЕ доказывает ja/ko-общность. + **Решение: Go-порт использует РЕАЛЬНЫЕ `unicode.RangeTable` `EstimateTokens`** (для общности §0.1); инвариантность + задокументирована для ДАННОГО zh-корпуса, не как свойство классификатора; ja/ko-паритет — верифицировать Go-таблицами + (после стройки). Коэффициенты не менять (impact на текущий zh = 0 подтверждён). + +### (г) Верификация ДО стройки ($0) — `ws2_fertility_verify.py` + `ws2_chunk_sim.py`, РЕЗУЛЬТАТ +- **Независимый пере-вывод фертильности** (НЕ доверяя `fertility_calib.json`): `f_cjk=1.1978, f_other=0.3852, + R²=0.9633, MAPE 4.25%, n=52` — Δ vs артефакт `< 3e-5`. Классиф.-mismatch impact на zh = **0.0%**. +- **Симуляция нарезки 25 глав:** гистограммы est_out; `target_out=1797` → **56 чанков** (mean 1408.8, max 1795) — тот + же СЧЁТ, что текущий input-est=1500, но **4 из 56 чанков ре-сегментируются (2 реальных абзац-сдвига**; проверено + прямым diff чанк-сетов: чанк 1 = 1777 vs 1778 симв, чанки 11↔12 и 54↔55 — абзац переезжает; ни один target_out не + даёт 0 diff). Смена единицы near-нейтральна на zh, НЕ no-op/byte-identical. Floor: 4 чанка < 500 симв (хвосты). + Edit-единица — **ДВА конфига симулированы (ревью-2 F1, пере-сим at 3200, `ws2_chunk_sim.py` ceiling∈{8000,3200}):** + **(i) ГЕЙТНУТЫЙ 8000-арм** — 25/25 глав-единиц в зоне 2k–8k out (0 глав > 8000, max 6528, hist: 14×[2k–3.2k]/10×[3.2k–5k]/ + 1×[5k–8k]); **(ii) РАТИФИЦИРОВАННЫЙ дефолт 3200** — 25 глав → **37 edit-единиц** (11 глав > 3200 делятся абзацно на 23 + под-единицы; hist out: 26×[2k–3.2k], 10×[0–1k] split-хвосты (< нижней зоны деградации — покрыты omission-бэкстопом), + 1×[1k–2k]). ⟶ «25/25 в 2k–8k» описывал 8000-арм, НЕ дефолт; при 3200 единицы МЕНЬШЕ (безопаснее для span-омиссии — + цель F1) ценой 10 мелких split-хвостов. **omission-бэкстоп (слой 5) = СЕТЬ, НЕ доказательство безопасности** (Q2a + span-омиссия НЕ гонялась; бэкстоп entity-уровня слеп к span-пропуску → размер edit-единицы гейтится, см. (б)/(е)). +- COGS-прогноз (no-cache upper bound): $0.403/25-гл; с кэшем — WS6. + +### (д) Тест-спека ПОСЛЕ стройки +- Пин: `est_out`-бюджет на zh даёт СЧЁТ 56 чанков (не byte-identical к input-est) + fresh↔resume детерминизм. +- Пин: мега-предложение → `OversizedSentence=true` флаг (синтетика). +- Пин: глава > `EditCeilingOut` → абзацный сплит (never-split-paragraph, tiling lossless). +- Пин (ревью-2 F8, испр.): `max_tokens`/`Reserve` edit-единицы сайзятся от ПОЛНОГО ЧЕРНОВИКА единицы (`prev`, + `EstimateTokens(prev)·MaxOutputRatio` + флор D24.3, D2.5) — НЕ от источника/`est_out`; синтетика: крупная глава, + переданная целиком как `prev`, получает unit-масштабный `max_tokens` без ложного length-ретрая. +- Golden re-capture: снос STMDepth/OverlapTokens + фертильность-бюджет = маскированный дифф старого↔нового снапшота + (только хеши/версии + новые поля). + +### (е) Открытое / маршрутизация +- `logical`-стратегия — СЕАМ, наполнение парковано (Q1). `DraftBudgetOut`/`EditCeilingOut` — tunable, ревизия по + пере-прогону. src→dst редакторский мэппинг — решён здесь (в план), не хвост. +- **Бюджет whole-chapter `Select` для W2-инъекции редактора (ревью-2 пост-верифай, открытый):** свежий Select по ВСЕЙ + единице над enriched-банком (§1(б)/F3) не имеет пиннутого token-бюджета; per-чанк `GlossaryTokenBudget` на всю главу + (много больше матчей) может вытеснить approved-канон и разбавить enriched-энфорс. Бэкенду: per-глава бюджет ИЛИ + unbounded для констрейнт-блока редактора (решить в пак-2). + +--- + +## §3. WS3 — Банк-машинерия W1.5 в Go (слой 4) + +Go-порт детектора V-C (WHICH) + алиас-ярус-1 + консолидация §C2 + эмиссия сид-дельты. **WHAT (dst) = НЕ ко-оккуренция** +(canon-recovery 0.20–0.68 < 70%) → dst через банкноту (WS4) + Палладий (имена) + облако (термы Z1/Z3). + +### (а) Что меняем (file:line) +- **новый `backend/internal/pipeline/miner*.go`** — детектор V-A→V-B→V-C (авторитет = exp16 `arms.py` FROZEN, НЕ + пересказ research/20 §B); алиас-ярус-1 (`alias.py` R1–R4); консолидация (`canon.py` §C2); эмиссия сид-дельты. +- **`memseed.go`** — `loadGlossarySeed` (`:64-186`) читает BASE-сид + сухую валидацию дельты (штампует `Source:"seed"` + `:131`); **но ПЕРСИСТ майненой дельты после подписи — ОТДЕЛЬНЫМ mined-write-путём** (штамп `Source:mined`, зеркало + `rubyToCandidates` `:314-360`), НЕ через `loadGlossarySeed` (F2, §1(в) — иначе `Source:seed` двигает `snapshot_W1`); + **новый `tmctl seed-lint`** (сухой прогон фейл-лаудов по дельте: `:87` src, `:94-98` dup, `:104-108` status, + `:113-115` draft/approved-dst, `:161-181` спойлер-оверлап, `:201-237` shared-key). +- **`store/glossary.go`** — `GlossaryEntry.Source` (`:36`, сейчас `seed|ruby|auto`) += **`mined`**; единая схема + кандидата ruby↔mined (research/20 §E-бэкенд-5). +- **`store/ruby.go`-паттерн** — прецедент `rubyToCandidates` (`memseed.go:314-360`: Source:ruby, status:auto, no dst, + Confidence=occ) зеркалится для mined-канала. +- **артефакты детектора** — контраст-словарь (jieba SHA `7197c321…`, вне git, воспроизводим из jieba 0.42.1 + `dict.txt`), паттерн-инвентари (`PACK_VERSION="zh-universal-v1"`), Палладий-таблица (~400 слогов, генерируемая). + +### (б) Алгоритм (frozen miner-v1 = exp16 §1.2 арм A3; ТОЧНЫЕ пороги) +**Детерминизм-контракт:** все ранкинги сорт `key=(-score, -freq, src)`; occurrence-count перекрывающийся (`pos=i+1`), +**suppress-free** (D24.2 — subsume чистит ТОЛЬКО ранжирование); norm = `normalizeSourceKey` (memnorm.go, trad2simp→ +NFKC→lowercase); Contrast-коллизия → MAX freq; Laplace `(cf+1)/(total_char+|distinct|)` идентичен в char_indep_rel и +detect_formants; Палладий-сегментация жадная longest-first, ъ отбрасывается. + +- **V-A:** кандидаты = Han-n-граммы **1–6** (БЕЗ сегментации); `freq_floor=3`; c-value nested-дисконт с + **`g(L)=log₂(L+1)`** (НЕ `log₂(L)` — иначе 蛊/转 зануляются, катастроф-скрин FAIL, 蛊 rank 4753); weirdness = + `p_book/max(word_rel, char_indep_rel, 1e-9)`; `score = max(cval,0)·termhood^1.0`, `termhood=log1p(weirdness)`; + subsumption `α=0.80` (дроп a если ∃ длиннее b, a⊂b, f(b)≥α·f(a)). +- **V-B:** + spread (Dice ко-оккуренция ru-лемм, pymorphy3) `score·(1+λ·spread)`, `λ=0.0` на инъект-черновиках + (spread ВРЕДИТ, конфаунд §D1), свип >0 только на cold-start; `spread_freq_min=5`; dst_variants `top=4, min_dice=0.05, + co-salience need=max(2, round(0.30·|chunks|))`. +- **V-C:** + паттерн-каналы (`patterns.py`): (1) 百家姓-якорь (SURNAMES_SINGLE после `.discard(凝)` + SURNAMES_COMPOUND + 16 вкл. 古月; окно 1–2 Han справа → name); (2) титул-суффиксы (TITLE_SUFFIX 26, ≤3 Han слева → title) + (2b) + ordinal_title (ORDINAL 15 + suffix → 四代族长); (2c) rank/grade (GRADE_PREFIX 甲…癸 / NUMERAL / RANK_WORD 等转阶级重 + 品段层 → title, ловит 等/转 форманто-невидимые); (3) топо-суффиксы (TOPO_SUFFIX 26 → place); (4) авто-формант + (`detect_formants`: символ c продуктивен если ≥`min_partners=3` различных морфем-партнёров И + `over_rep=p_book/p_gen≥15.0` — над-представленность, НЕ редкость; изолирует 蛊/窍/虫, отвергает 师/花/等); (5) + Палладий ru-канал (`is_palladius_token` + кап-лемма-гейт `is_name_lemma` → блок «найти»=най+ти; бонус +56.0). + Бонусы `{name/place:140, title:120, term:80}`×pattern_source_weight `{surname:1.0, ordinal_title:1.0, + rank_grade:1.0, title_suffix:0.9, topo_suffix:0.9, palladius:0.8, formant_suffix:0.6, formant_prefix:0.5, + title_bare:0.4, default:0.3}` (pw = МАКС по evidence); sub-floor `score=bonus+40.0·f·pw`. `top_k=90` операционная точка. +- **Алиас-ярус-1 (R1–R4, `alias.py`, precision-safe, R4 hard-blocks ПЕРВЫМИ):** R4-ii (разный пол, кроме hidden) → + R4-iii/v (разный approved dst → разные сущности ДАЖЕ при containment: блок `族长⊂四代族长` [D38 §3], `古月⊂古月方源`) → + R1 containment (|X|≥2 → extension; **компози-гард**: long=short+Y, Y∈{title,place,term}∩gt → ФРАЗА не алиас, блок + `古月+族长`, сохрани `古月+方源`) → R2 фамильный якорь → family (не identity; R4-i разные имена) → R3 общий ru-рендеринг + → identity (**кап-лемма-гейт**: Палладий-токен И name-lemma min_syl=2). R4-iv `cooccur_same_sentence`. Кластеризация + union-find по identity-рёбрам, `sorted`. Метрика — **B³ (entity-level), НЕ MUC**. +- **Консолидация §C2 (`canon.py`):** канон по ВСЕМ вхождениям (НЕ first-wins, анти-LTCR) × Палладий-конформность + (name/place) × полнота падежной леммы. `canon_recovery cover_frac=0.5`. **Майнер по построению НЕ пишет approved** + (канон-предложение = draft). Пол = 他/她-счётчики evidence, вердикт владельца (D19.3, hidden невыводим). since_ch = + первая глава вхождения (авто); until_ch — только человек. +- **Эмиссия сид-дельты §C2-7:** YAML в схеме `seedTerm` (`memseed.go:30-57`, БЕЗ новых полей): non-seed кандидат с + banknote/canon dst → `status:draft` (dst, ⟨проверить⟩); WHICH-only без dst → `status:auto` (инертен). Алиасы — + поверхностями терма. Evidence/Source-класс/зоны Z1–Z5 — в сайдкар-карту подписи (прецедент + `diag/glossary_v2_signoff.md`), НЕ в схему сида. Пре-валидация против фейл-лаудов loadGlossarySeed = часть карты. + **Эмиттер обязан реюзить subsumption+fragment-фильтры** (`emit_owner_sheets.build_precision30`: тип∈{name,place, + title}, freq≥5, src∉subsumed, len≥2, не фрагмент по PARTICLE-сету), а НЕ сырой 13618-дамп (WS3 (г) находка: сырой + дамп несёт §B2-фрагмент-шум). +- **Плагин-сеамы P1–P6 (§B5):** данные/конфиг per язык/пара (идиом промпт-паков `PromptPathFor`), НЕ ветки кода. + Наполнение — zh(-ru) инстансы + существующий ruby-канал ja. Политика: **degrade-with-flag** (P2–P6, потеря recall + видима в карте подписи), **fail-loud ТОЛЬКО P1** (нет генератора кандидатов → майнинг для языка не заявляется). + P5 = лемматизатор цели (см. ru-лемматизация ниже). P4 = таблица транслитерации по ПАРЕ (Палладий zh→ru). +- **ru-лемматизация (§B5 P5) — Go-прод-путь W1.5 (ИСПРАВЛЕНО ревью-1 F2, прежняя формулировка «λ=0 → лемматизатор не + нужен» ОПРОВЕРГНУТА):** `λ=0` глушит spread-МНОЖИТЕЛЬ, НО `dst_variants` считается БЕЗУСЛОВНО λ (`arms.py:103` + `_dst_of` при `freq≥5`), а Палладий-канал V-C потребляет `dst_variants[:2]` + `is_name_lemma` (`arms.py:124-131`) — + обе pymorphy3-backed (`spread.py:45-60,117-143`). ⇒ **Палладий-подканал V-C НУЖДАЕТСЯ в лемматизации даже при λ=0.** + Три варианта (owner/бэкенд-развилка §10): **(A) faithful pymorphy3-порт** (dst_variants + is_name_lemma) → Go + воспроизводит golden ТОЧНО, вкл. Палладий-бонус +56.0 → `古月:21`; цена — нетривиальный порт лемматизатора; **(B) + дропнуть Палладий-подканал в Go-дефолте** → `古月` теряет +56.0, ранг **21→22** (катастроф-СКРИН ∈top-50 ВСЁ ЕЩЁ PASS, + но exact-ранг не воспроизводится); **(C) surface-аппроксимация** (cap-предоминантность по поверхности + стеммер для + dst_variants) — грубее, паритет НЕ exact. **Рекомендация (обновлено ревью-2 к ратифицированному §10-14 дефолту): + дефолт (B)** — дроп Палладий-подканала с явным пере-пином фикстур (`古月:21→22`, скрин PASS; dst доставляет банкнота, + exact-ранг НЕ продуктовая ценность); (A) faithful pymorphy3-порт — ТОЛЬКО при готовой поддерживаемой Go-морфологии. + omission-бэкстоп слоя 5 (WS2/WS5) — отдельно: substring- + матч по `decl.forms`+dst + лёгкий Snowball/Porter-ru-стеммер (Go-либа, детерминирован), pymorphy3 НЕ требуется там. + +### (в) Конфиг + снапшот +- Пороги miner-v1 = **СТАРТОВЫЙ версионируемый конфиг** `mining-v1` (свип ±50% инвариантен — A3@f≥3=1.0 держится по + floor/over_rep/subsume/partners). `PACK_VERSION="zh-universal-v1"`. +- Майненые auto/draft строки: при `PostcheckGate=false` НЕ двигают `MemoryVersion` (`memory.go:264-266`); подписанные + approved → двигают `snapshot_W2` (§1 (в)). Строка манифеста §8. +- `Source:"mined"` — новое значение enum (аддитивно, миграция store). + +### (г) Верификация ДО стройки ($0, несущая) — `ws3_miner_verify.py` + `ws3_seeddelta.py`, РЕЗУЛЬТАТ +- **(г) до-стройки:** Python miner-v1 на запинённых входах exp16 (§1.10 SHA: 8 код-файлов ✅, jieba `7197c321`, сид + `175a67ad`, records `3de49eaf`) воспроизводит golden: катастроф-ранги `{方源:0,蛊:1,蛊师:2,古月:21}` EXACT, **13618 + кандидатов** EXACT, recall@proposed full **0.9649**≈0.965, test-half **0.9318**≈0.932, top-1 方源 score 6640.9. + ⚠ **Паритет-фикстура РАСЩЕПЛЕНА (ревью-1 F1) — нельзя пинить `古月:21` exact И дропать pymorphy3:** (i) **Палладий- + ИНВАРИАНТНЫЕ гарантии** (Go-порт обязан воспроизвести при ЛЮБОМ ru-лемм-решении): recall@proposed 0.965/0.932, + 13618-кандидат-СЕТ (членство), катастроф-СКРИН ∈top-50; (ii) **Палладий-ЗАВИСИМЫЕ ранги/скоры** (古月 rank/score, + прочие +56.0-кандидаты): пинить exact ТОЛЬКО при варианте (A) faithful pymorphy3-порт; при (B)-дропе — пере-пин + (古月→22, скрин PASS). `方源/蛊/蛊师` (ранги 0/1/2) Палладий НЕ трогает → инвариантны. +- **Юнит-фикстуры алиас-правил** (exp16 §3.2): 族长≠四代族长 блок, кап-лемма-гейт «найти», компози-гард — в `alias.py` + (recall известных 2/2). Прогон против Python-реализации — часть фикстур порта. +- **Эмиссия сид-дельты §C2-7** (exp16 её НЕ эмитил — только сайдкары): **61 терм** (21 draft+banknote-dst / 40 + auto-без-dst); **сухой линт против фейл-лаудов memseed = 0 проблем** (loadable). Артефакт + `books/gu-zhenren/design11/ws3_seed_delta.yaml`. Находка: draft-кандидаты флагают ТОЧНЫЙ D38-дефект + (`古月族长→старейшина`, `甲/丙`-грейд) как `draft ⟨проверить⟩` для подписи — дисциплина «майнер не пишет approved» + РАБОТАЕТ; auto-кандидаты несут §B2-фрагмент-шум → эмиттер обязан реюзить subsumption+fragment-фильтры. +- **(д) исполнение самого паритета Go↔Python** закрепляет бэкенд после стройки (метод mem_select.py в обратную + сторону: Python = референс; при расхождении чинить Go-порт). + +### (д) Тест-спека ПОСЛЕ стройки +- **Байт/значение-паритет Go↔Python miner-v1:** Go-порт воспроизводит Палладий-ИНВАРИАНТНЫЕ гарантии (13618-СЕТ, recall + 0.965/0.932, катастроф-СКРИН ∈top-50) — фикстуры (г). Палладий-ЗАВИСИМЫЕ exact-ранги (古月:21) — только при варианте + (A) pymorphy3-порт; при (B) — пере-пин. Начать с `normalizeSourceKey` (реюз `memnorm.go`, уже байт-верифицирован + через `mem_select.py`). +- Юнит-тесты алиас-правил (5 R4-блоков + компози-гард + кап-лемма-гейт). +- `tmctl seed-lint`: прогон эмитированной дельты через РЕАЛЬНЫЙ `loadGlossarySeed` (не Python-зеркало) → 0 фейл-лаудов. +- **Пин `Source:mined` (ревью-2 F2):** reseed W1.5-дельты штампует `Source:mined` (НЕ `seed` — `loadGlossarySeed` + хардкодит `Source:"seed"` `memseed.go:131`); проверить `base-bank-version` = хеш `Source∈{seed,ruby}` СТАБИЛЕН после + вливания mined-дельты (иначе `snapshot_W1` двигается → «переоплата ОДНА» сломана). +- Детерминизм: повтор Go-майнера байт-в-байт (без time/rand). + +### (е) Открытое / маршрутизация +- `λ`-режим (spread-коэф): дефолт 0 (инъект); свип на реальных не-инъект черновиках пере-прогона (§10). +- ru-лемматизатор P5 — полный pymorphy3-паритет парковано; дефолт-майнер обходится (см. (б)). +- Мини-голд алиасов владельца (§E-1) — recall кластеризации; без него ярус-1 только по precision (§10). +- precision@30 non-seed A3 — адъюдикация владельца (§10). +- Z1-адъюдикация → ОБЛАКО (локальная 9B wrong-catch 0.333, принимает 族长→«старейшина»), НЕ локаль. Локальный + 9B-споттер НЕ строим (код 0.965 ≥ локаль 0.895). + +--- + +## §4. WS4 — Банкнота: dst-канал (слой 4, wire-инвариант) + +Спека banknote-v1 = research/20 §B3 / exp16 `banknote.py`: разделитель `⟦TM-BANK-v1⟧`, ≤`MAX_LINES=12` строк +`srcdsttype` (type∈{name,place,title,term}, парсер +nickname), «только НОВОЕ» (нет новых → нет разделителя). +Строить канал ТОЛЬКО если §D-правило «сноска оправдана» сработало. **РАТИФИЦИРОВАНО D39.10** как dst-канал: parse_fail +0% (<5% ✓); ниша = **ПРЯМАЯ доставка dst** (переводчик эмитит 蛊→гу, non-seed 龙公→Лун Гун — ровно то, что ко-оккуренция +извлечь НЕ смогла), НЕ recall (маржин f<3 +1 терм ~10 п.п. — на пороге; код сильнее: f<3 8/10 vs банкнота 5/10); +Δкачества mean 0.935 под шум-полом 0.261 → уверенной деградации нет. Цена ~$0.01–0.1/ранобэ. + +### (а) Что меняем + 12 интеграционных точек (file:line, HEAD, каждая с тест-спекой в (д)) +1. **Санитайзер (`sanitizer.go` v6):** на draft-стадии НЕ гоняется (isFinal-only, `chunkrun.go:48`). ⚠ Существующий + `trailingNoteRE` (`:240`) НЕ ловит `⟦TM-BANK-v1⟧`/`srcdst`-строки (только «Примечание/Сноска»-заголовки), а + CJK-leak (`:76-84`) фаерит только на голых Han-ранах — смешанная банкнот-строка «蛊 гу term» может НЕ сработать + (ревью-1 F3: НЕ полагаться на trailing-note как бэкстоп). **ПЕРВЫЙ и единственный надёжный рубеж — телеметрия п.10 + `banknote_parse_fail` ДО редактуры.** Если нужен финал-бэкстоп — добавить ЯВНОЕ версионируемое `⟦TM-BANK-v1⟧`-правило + санитайзера (не переиспользовать trailing-note). +2. **Санитайзер cjk_leak (v6):** src-колонка = Han по построению → срез ДО санитайзера снимает ложное срабатывание; + gloss-whitelist v6 НЕ трогаем. +3. **classify/echo-детект** (`disposition.go`, `const cjkEchoThreshold=0.15`): считать по ОЧИЩЕННОМУ тексту (срез → + classify(stripped)). Порядок ЖЁСТКИЙ. +4. **Coverage-гейт** (`coverage.go`, translator-only `chunkrun.go:73`): `len_ratio` числитель по очищенному (сноска + ≠ перевод). +5. **max_tokens-сайзинг** (`stagerun.go:87-100`): += бюджет блока (≤12 строк × ~12 ток). Перевод ПЕРВЫМ, блок хвостом + → трункация бьёт по блоку; парсер fail-tolerant на последней строке + флаг `banknote_truncated`. +6. **request_hash/снапшот** (`render.go` RequestHash, `snapshot.go`): ⚠ **(ревью-1 F2) `PromptSHA256` (`:238`) фолдит + ТОЛЬКО файл промпта** — если инструкция сноски = config-флаг, аппендящий её при рендере (как `few_shot`), PromptSHA256 + не двигается, wire тихо расходится (`snapshot.go:110-116`-класс). **Решение (а): запечь инструкцию в ФАЙЛ промпта** + (тогда PromptSHA256 реально двигается) ИЛИ явный stage-флаг-фолд. **(б) Версия ПАРСЕРА/среза банкноты — verdict-ось** + (управляет resolved-выходом черновика + derived-hash, как `ClassifierVersion` `:173` / `sanitizerSnap` `:66-77`): + добавить `banknoteSnap{enabled, parser_version}` в `snapshotID()` (единое имя поля, ревью-2 F9-унификация; json-тег в + Go — как `sanitizerSnap.version`, решает бэкенд) — иначе bump парсера без правки промпта оставляет снапшот + тем же (свежие чанки режут новым парсером, резюмнутые отдают старый derived → тихая внутри-книжная дивергенция). + Включение канала = громкий `--resnapshot`. +7. **Чекпоинт/резюм** (`stagerun.go:66-80`): resume передоказывает derived-артефакт из сырого чекпоинта бесплатно + (content-addressed hash → тот же id). +8. **«Редактор её не видит» (НЕСУЩАЯ, главная правка кода):** свежий прогон — редактор читает `prev=sr.Text` + (`chunkrun.go:162`), хук подменяет на очищенный. НО на резюме OK-путь берёт текст по `final_hash`, который сегодня = + СЫРОЙ attempt (`stagerun.go:168-169`: `if disposition==DispOK { finalHash=last.reqHash }`); re-point на derived + существует ТОЛЬКО для flagged (`:170-185`). **ТРЕБОВАНИЕ: final_hash OK-стадии draft ПЕРЕНАПРАВИТЬ на + `tm-banknote-v1` derived-чекпоинт** (расширение прецедента flagged→ok). Без этого резюм отдаёт редактору сырой блок. + ⚠ **Эскалационный путь (ревью-1 F1): draft-стадия ЭСКАЛИРУЕТ** (`pipeline-c1.yaml:53` `escalate_to: deepseek-v4-pro` — + живое, не гипотеза; `pipeline.go:374` разрешает escalate_to только translator). На эскалированном OK-черновике + `last=esc.fb` (`stagerun.go:159`), `finalHash=last.reqHash` = СЫРОЙ эскалированный чекпоинт, `sr.Text=last.text` + кормит редактора СЫРЬЁМ. **Срез+derived-commit+`sr.Text`=stripped+re-point ДОЛЖНЫ стоять ПОСЛЕ резолва эскалации + (`stagerun.go:163`) и потреблять `last.stripped`** — тогда эскалированный черновик тоже очищен. (`resume.go:32-52` + читает `GetCheckpoint(final_hash)` прозрачно → правка resume.go НЕ нужна.) +9. **Пост-чек глоссария** (`mempostcheck.go`, по финалу редактора): не влияет; но `renderGlossaryBlock` (`memory.go:477`) + идёт в промпте ДО инструкции сноски («новые = не из этого списка»). +10. **Телеметрия (лауд):** per-chunk `n_banknote_lines`, `banknote_parse_fail`, `banknote_truncated` (в retrieval_state- + стиле). +11. **Провайдер-квирки:** DeepSeek thinking-ON НЕ трогать (`models.go` `echoMineViolation`, `LoadModels` fail-fast). + §D-арм меряет дельту качества черновика с/без инструкции (гейт канала; exp16: mean 0.935, под шум-полом 0.261 → + уверенной деградации нет, но пере-замерить на пере-прогоне). +12. **Экспорт/полигон:** `tmctl export` отдаёт final без сноски автоматически (срез раньше); кандидаты — из + артефакт-файла, не текста. + +### (б) Алгоритм +- **Срез (`split_banknote`):** `idx=output.find(SEP)`; <0 → `(output.rstrip(), "")`; иначе `clean=output[:idx].rstrip()`, + `block=output[idx+len(SEP):].strip("\n")`. Срез ПЕРЕД `classifyOutput` (`chunkrun.go:32`) на сыром draft-output. +- **Парс (`parse_banknote`):** строки `block.split("\n")` непустые; толерантный field-split `\t| {2,}|\s*\|\s*`; <2 + полей → если последняя строка И `truncated_generation` → `banknote_truncated`, иначе `bad+=1`; `typ=parts[2].lower()` + (≥3 полей) иначе `term`, не-`_TYPE_OK`→`term`; **src ДОЛЖЕН содержать Han** `[㐀-鿿]` иначе bad; `parse_fail=(bad>0)`. +- **finish=stop-only гейт (обязателен, errata exp16 §6):** генерация банкноты принимается ТОЛЬКО при `finish=="stop"` + (не «≠length» — DeepSeek peak-overload даёт `insufficient_system_resource`). При stop-only truncation-толерантность + парсера в проде НЕДОСТИЖИМА (гейт отклоняет не-stop) — но держать в парсере для устойчивости. +- **derived-checkpoint (ТОЧНАЯ формула прецедента `commitSanitizedExport`, `stagerun.go:230-232`):** + `sha256("tm-banknote-v1\x00" + reqHash + "\x00" + stripped)`, id-префикс `"tm-banknote-v1:"`, через + `PutDerivedCheckpoint` (`ledger.go:182`, cost=0, escalation=0, idempotent), `UsageJSON:"{}"`, + `FinishReason:"banknote_export"`. (НЕ упрощённая формула доки без NULL-байтов.) +- Распарсенные кандидаты → отдельный артефакт-файл (evidence для W1.5 §C), НЕ в store до подписи. + +### (в) Конфиг + снапшот +- Включение канала: инструкция запечена в ФАЙЛ промпта (→ `PromptSHA256` двигается) ИЛИ stage-флаг-фолд; ПЛЮС новый + **`banknoteSnap{enabled, parser_version}`** в `snapshotID()` (verdict-ось, зеркалит `sanitizerSnap`) — не полагаться на + `PromptSHA256` (ревью-1 F2). Смена парсера/среза = громкий `--resnapshot`. Строка манифеста §8. +- derived-hash неймспейсит версию канала → смена версии = новый id (не коллизия). + +### (г) Верификация ДО стройки ($0) — `ws4_banknote_verify.py`, РЕЗУЛЬТАТ +- **Пере-парс 95 строк** (14 сохранённых сырых выходов exp16): 95 строк / 65 distinct_src / **0 parse_fail / 0 + truncated** — EXACT golden (parse_fail 0% воспроизведён). +- **Синтетическая трункация:** под `truncated_generation=True` → 2 годные строки + `banknote_truncated` (не bad); под + `False` → `banknote_parse_fail` (корректно). +- **fresh↔resume эквивалент-тест (спека):** `tm-banknote-v1:` id = content-addressed (формула с NULL-байтами) → fresh + и resume дают ИДЕНТИЧНЫЙ id; смена stripped → другой id. **Re-point OK-пути final_hash resume-стабилен конструкцией.** +- **Инварианты парсера:** SEP не note-слово; non-Han src=bad; no-SEP→всё clean; толерантный сплит (таб/≥2проб/pipe) — + все PASS. + +### (д) Тест-спека ПОСЛЕ стройки +- Каждая из 12 точек — пиновый тест (срез ДО classify; cjk_leak/coverage/echo по очищенному; max_tokens+блок; + derived-hash формула; **re-point OK-пути final_hash → derived** [fresh↔resume эквивалент]; телеметрия 3 метрики; + thinking-ON нетронут). +- Пин: parse_fail 0% на 95 строках; truncation-толерантность (синтетика); SEP не встречается в чистом тексте. +- Пин: включение канала = снапшот-сдвиг (промпт-SHA + константа). + +### (е) Открытое / маршрутизация +- Дельта качества перевода от форматного констрейнта — пере-замер на пере-прогоне (§11-провайдер §D-арм). +- Ниша канала = ДОСТАВКА dst, НЕ recall (код f<3: 8/10 vs банкнота 5/10). Z2-дрейф банкнотного dst (转→«оборот» vs + approved «ранг») — ловится подписью владельца (draft, не approved). + +--- + +## §5. WS5 — Дефект-класс чекеры + данные пакета пары (слои 5 + 2-данные) + +Дефект-класс-леджер D39.8 (все пак/банк/гейт-уровня, НЕ нарезка). Действие при срабатывании: **флаг / констрейнт- +инъекция — НЕ тихая правка текста** (диспозиция D2 цела). Гейты — ДЕТЕРМИНИРОВАННЫЕ ($0): судьи слишком шумны +(пол 0.126 когезия / 0.261 смысл — судьи только НОМИНИРУЮТ). + +### (а) Что меняем (file:line) — по классу: где живёт правило (данные пакета vs код гейта) +- **`cheapgates.go`** — расширяется дефект-классами DC1/DC2/DC7 (детерм. флаггеры на финале, `chunkrun.go:199-207` + реюз `runCheapGates`, target-gated `isRuTarget`, наблюдаемость не гейт по дефолту). +- **новый omission-бэкстоп** (слой 5, компенсатор зоны деградации WS2) — reflow-инвариантный лемма-матч. +- **данные пакета пары zh-ru** (слой 2, версионируемый) — таблицы конверсии единиц (DC1), негатив-лист регистра (DC6), + verse-локусы (DC5). +- **`memory.go`/`renderEditorConstraintBlock`** — gender-инъекция (DC3) из поля сида `Gender` (`glossary.go:26`, + `memseed.go:35`, folded в F1 `memory.go:273`). + +### (б) Алгоритм + верифицированный статус ланд-абельности +- **DC-1 时辰/меры (данные пакета + детерм. чекер):** таблица конверсии `时辰=2ч`, `万/億`-порядок, `里/斤/суй-1`; + извлечь числовое время из src (`N个时辰`), сверить рендер (`三个时辰≈6ч`, НЕ «три часа»). Рефайн: флагать ТОЛЬКО явный + mismatch (рендер счёта как часов), не «часы не найдены». ⚠ **(ревью-1 A1) сработка на 25-гл — ОДНА (n=1, ch1/chunk4), + РЕАЛЬНЫЙ дефект (`3个时辰→3 часов`) на КОРПУСЕ-ОТКРЫТИЯ этого дефекта → «true FP≈0» преждевременно (n=1 ≠ rate). + Landing ГЕЙТИТСЯ §5(д)-FP на СВЕЖЕМ пере-прогоне** (не текущий rerun). Конверсия детерминирована → низкий-FP ожидаем. +- **DC-2 масштабы `千万/数十万` (b1-класс):** `千万=10⁷`, `数十万=неск.×10⁵`; + word↔word alignment (`四成四=44%`, + `三成=30%`, `十二分之一=1/12` — реюз `q4a_traps.rule_b1/rule_b2` + LOCATOR). Это то, что `regressionguard.go` НЕ + умеет (арабские цифры только, `L5-regressionguard` слеп к word↔word). ⚠ **(ревью-1 A4) `千万` — стоковая ГИПЕРБОЛА + («несметно/тьма»), «тысячи» — in-register литературный рендер, НЕ жёсткая ошибка как DC1/DC6.** НЕ bundle с DC1/DC6 + как «true FP≈0»; магнитуд-чекер — самый hyperbole-exposed → замерить FP ОТДЕЛЬНО в §5(д). Landing ГЕЙТИТСЯ §5(д). +- **DC-3 gender — РАЗВЕСТИ инъекцию и чекер (ревью-2 F6):** + **(1) Инъекция из поля сида `Gender` в констрейнт-блок редактора — ЛЕНДИТСЯ для ВСЕХ ТРЁХ состояний, ВКЛЮЧАЯ + hidden-мандат:** male/female ⇒ жёсткие родовые формы; hidden ⇒ мандат БЕЗРОДОВЫХ конструкций / surface-`他` (→ «он») + ДО `until_ch` (D19.3, `白凝冰`) — ЭТО и есть фикс класса Бай Нинбин: инъекция НАПРАВЛЯЕТ редактора, кореференция НЕ + нужна. **(2) Чекер (наблюдаемость на финале):** male/female-чекер ЛЕНДИТСЯ с FP-гейтом §5(д), паратекст-чанки + (foreword/синопсис) исключены. **hidden-ЧЕКЕР НЕ лендится** — ⚠ (ревью-1 A3, УТОЧНЕНО пост-верифай по records.json): + FP 3.5% (2 сработки) НЕ обе паратекстные: `ch1/chunk0` = паратекст-книга-референс («её»→«эту книгу»), НО `ch1/chunk5` + («Бай Нинбин… её дар просто пугает», сцена совета старейшин, НЕ паратекст) = РЕАЛЬНАЯ narrative-coref-неоднозначность + женской формы на HIDDEN-персонаже — это СИЛЬНЕЕ довод к Ф2, не слабее. Паратекст-исключение снимает ТОЛЬКО chunk0; + chunk5 остаётся → нужна РЕАЛЬНАЯ кореференция (→ Ф2). until_ch/sentence-scope НЕ фиксит. ⟶ Не лендится ТОЛЬКО + hidden-ЧЕКЕР (инъекция всех трёх — лендится); прежняя формулировка «лендится только male/female энфорс» смешивала — + бэкенд не построил бы hidden-инъекцию. +- **DC-4 род говорящего в диалоге:** Ф2-annotator (дискурс-уровень, НЕ поле сида — говорящий может быть не в + глоссарии). Трап «я мечтал»→«мечтала». **Вне детерм.-скоупа слоя 5 → Ф2** (диспозиция §9). +- **DC-5 стих/аллюзии — ⚠ НЕ ЛЕНДИТСЯ детерм. (верификация):** loose line-profile детектор ловит трап но **FP 73.7%**; + strict (регулярный метр + поэт-лексема) → FP 42.1% НО теряет позитив. ⚠ **(ревью-1 A5)** ДВЕ протестированные профили + не лендятся (best = 42% FP + потеря позитива; 排比↔стих-неоднозначность трудна); универсального «детекция неспособна» + из n=2 не утверждаем. **Решение (консервативный дефолт): DC-5 = пак-локусы** (владелец курирует известные verse/ + allusion src-строки) + **рендер-мандат «не сплющивать» + слот сноски** + опц. LLM-НОМИНАЦИЯ (не гейт). Продукт-решения + (рендер-политика, сноски) — **владельцу** (§10). +- **DC-6 регистр-лексикон (негатив-лист данных пакета):** `{терем-класс}` — сказочно-русский/канцелярский слой, + ломающий жанр. Чекер вхождений на финале + замены. Сработка на 25-гл — ОДНА (ch1/chunk4), РЕАЛЬНЫЙ дефект («терем» в + сянься — жёсткая регистр-ошибка). ⚠ (ревью-1 A1) n=1 ≠ FP-rate; **Landing ГЕЙТИТСЯ §5(д)-FP на свежем прогоне** + (негатив-лист по построению низкий-FP). Негатив-лист — данные пакета (расширяемо владельцем). +- **DC-7 editor-инверсии grade/role (сверка на ВЫХОДЕ редактора):** канон-грейд `甲乙丙丁→«класс А/Б/В/Г»` (не Б/2/A), + титул `族长→«глава клана»` (не «старейшина»). Точный канон-матч против editor-output → низкий FP ожидаем. ⚠ **(ревью-1 + A2) DC-7 НЕ имеет $0-верификации** (ws5-скрипт не тестирует его) → **landing UNVERIFIED, FP только в §5(д)** — не + утверждать «ЛЕНДИТСЯ» до замера. Корень (D38 §3): promote `甲乙丙丁`+`四代族长` **draft→approved** + reseed (owner-only) — + иначе `renderEditorConstraintBlock` (CONFIRMED-only) не инъектит, `suppressContained` съедает верный `族长`. +- **Omission-бэкстоп (слой 5, WS2-компенсатор):** reflow-инвариантный лемма-матч (`reflow_omission.py`): per-source + ENTITY присутствует ли ГДЕ-УГОДНО в target (position-independent → reflow-safe); verdict-каскад skip→present→partial + →omitted; `inflection_gap` FLAG (лемма совпала, surface вне pinned decl.forms — НЕ omission, иначе ~7-8% ложных); + content-floor `ratio=len(tgt)/(1.3·len(src))<0.5` крайний backstop. Правило: entity-omission≈0 = «INSTRUMENT + CEILING» НЕ «size safe» (D32.4). Go-путь БЕЗ pymorphy3: substring-матч по `{dst, decl.forms, aliases}` (детерм.), + флаг при отсутствии; inflection-толерантность через лёгкий стеммер (см. WS3 (б) P5-решение). `RegressionGuard`/ + coverage тут смещены (editor легально мержит — research/19 §B0-4). + +### (в) Конфиг + снапшот +- Данные пакета пары (таблицы единиц, негатив-лист, verse-локусы) — версионируемый пакет zh-ru, снапшот-folded + (правка = громкий resnapshot, как `chunkerVersion`). Строка манифеста §8. +- gender-инъекция (ревью-2 реконсиляция): ДАННЫЕ пола (`Gender` seed-поле) фолдятся через `MemoryVersion` (правка пола + персонажа → снапшот); РЕНДЕР-ЛОГИКА (пол→констрейнт-текст в `renderEditorConstraintBlock`) фолдится тем же + **выделенным `render-format-version`-компонентом**, что и src→dst-блок (НЕ `memoryMatchVersion` — §2(в)/§8). +- Дефект-чекеры (cheapgates) версионируются `StyleCheckVersion` (`snapshot.go:207`, наблюдаемость → resnapshot при + правке правила). +- DC-7 promote `甲乙丙丁`+`四代族长` draft→approved = reseed → `MemoryVersion` двигается (снапшот). + +### (г) Верификация ДО стройки ($0) — `ws5_checkers_verify.py`, РЕЗУЛЬТАТ +- **(а) Позитивы: 6/6 пойманы** (синтетика exp15 §7.9/§2: `三个时辰→«три часа»`, `千万→«тысячи»`, `数十万→«десятки + тысяч»`, «терем», стих[loose], gender-hidden). Точные zh-трап-строки (exp14b §2 таблица a1..d2 + 时辰/千万) — фикстуры. + ⚠ **DC-7 (grade/role) НЕ покрыт $0-скриптом** (ревью-1 A2) → его landing verifies только §5(д). +- **(б) FP-fire на 25-гл финале** (корпус records.json final; §5(д)-замер = `tmctl export` после стройки — rerun-store + схема v7 < HEAD v8, read-only не мигрирует; exportNormalize косметичен, не меняет слова чекеров): **DC1/DC2/DC6 = по + 1 сработке (n=1, все на ch1/chunk4 — реальные дефекты, но n=1 ≠ FP-rate, ревью-1 A1)** · **DC3 3.5% (2 — 1 паратекст + book-ref + 1 narrative-coref на hidden-персонаже, A3-уточ; male/female-чекер $0-неверифицирован как DC-7 → §5(д))** · + **DC5 loose 73.7% / strict 42.1% → НЕ детерм.-гейт**. ⟶ Разделение: DC5 НЕ лендится + (эскалация владельцу); DC1/DC2/DC6/DC7 landing ГЕЙТИТСЯ §5(д)-FP на СВЕЖЕМ прогоне (текущий n=1 — недостаточно); + DC3 — только male/female seed-field. + +### (д) Тест-спека ПОСЛЕ стройки +- **FP-замер через `tmctl export`** (инвариант №8) на СВЕЖЕМ пере-прогонном 25-гл финале (не records.json): каждый + чекер (DC1/2/3/6/7) — измеренный FP-rate в отчёте; чекер с заметной долей ложных не лендится. ⚠ **Пред-условие + (ревью-1 A6): DC1/DC2 нужен SOURCE-столбец** (для `时辰`/`千万` сверки src↔target); `tmctl export --pairs` на HEAD + НЕ существует (research/20 §E-бэкенд-3) → построить `--pairs` (source-join в Export уже есть) ДО §5(д)-замера DC1/DC2. +- Позитивы из эмпирики (exp15 §7.9 / exp14b / exp16) обязаны ловиться (регресс-фикстуры). +- Sentence-scoping обязателен (q4a_traps F4: скорить ОДНО предложение, эндеры `.!?…\n` — иначе raw-draft и merged-glm- + абзац несопоставимы). Три исхода fix/fail/«?»; «?» ИСКЛЮЧАЕТСЯ из rates, никогда тихий pass. + +### (е) Открытое / маршрутизация — ВЛАДЕЛЬЦУ (§10) +- **DC-5 рендер-политика стиха + слот сноски** (детерм. детекция не работает — эмпирика): продукт-решение владельца. +- **DC-4 род говорящего** = Ф2-annotator (диспозиция §9: Ф2-очередь). +- Промоут `甲乙丙丁`/`四代族长` draft→approved — owner-only (approved-инвариант). + +--- + +## §6. WS6 — Свап-арм редактора + готовность пере-прогона + +### (а) Что меняем (file:line) +- **`configs/pipeline-*.yaml` / `models.yaml`** — армы = КОНФИГ, не код: editor-стадия `model: glm-5` (базлайн) / + `mistral-large-latest` / `deepseek-v4-pro` + `few_shot` тумблер (`pipeline.go:103` `FewShot *bool`); снапшот per-арм + (разный editor-model → разный `stageSnap.Model` → разный `snapshot_W2`). +- **`export.go`/`quality.go`** — атрибуция арма в export/report (editor-model уже в chunk_status/checkpoint). +- Инвариант: editor pinned (D12, `pipeline.go:373`: escalate_to только на translator) — свап = смена editor-модели + конфигом, НЕ эскалация. Пинится `echo_mine_test.go` (сейчас требует glm-5 обоих пайплайнов; тест обновляется под + арм-варианты). + +### (б) Алгоритм (прогон-протокол — предпосылки для будущего полигон/монитор-промта) +- Армы идут ЧЕРЕЗ прод-путь со ВСЕМИ гейтами (урок V2-брака §7.9: eval-риг без прод-гейтов [санитайзер v6 поймал бы + 待遇/一步登天] дискредитировал арм). +- **Пред-условие mistral-арма (ревью-2 F4):** `mistral-large-latest` НЕ идёт через прод-путь до пер-модельного + rate-guard (§1(б)) — ~48% retry-fails @1.3s без него исказят и латентность, и денежную телеметрию арма. glm-5/ + deepseek-pro-армы (grok-семейство 0% retry-fails) rate-guard не блокирует, но семафор общий по конфигу. +- Инструмент-фиксы слепых пакетов (D39.8 п.3): авто-QA до человека (CJK-в-ru / битые формы / род-консистентность); + обрезка ВСЕХ версий по общему финальному предложению; 2-way для тонких контрастов; **тир-структура претензий** + (критические/смысловые/редакторские) вместо плоского «≤2». Планка — 2 претензии (интерим, не пилот Ф2.5). +- Любое судимое сравнение наследует ратифицированный риг-стандарт (D39.7): per-vote персист, полно-evidence окна + (HEAD_CHARS-класс запрещён), оба порядка, floor-гейт маргиналов, DET-primary > судьи где возможен (валидирован + D39.9); генерации — только `finish=stop`. +- ⚠ **Полы `0.126`/`0.261` — ИСТОРИЧЕСКИЕ калибровки, НЕ переносимые константы (ревью-2 F5, замечание владельца):** + измерены в glm/flash-условиях на S2′/exp14-материале. Каждый СУДИМЫЙ арм-контраст пере-прогона меряет СВОЙ пол + A0↔A0′-блоком (~$1/арм) на материале ЭТОГО прогона ДО интерпретации маргиналов — per-editor-model И per-материал. + Смягчающее: **главные оси floor-иммунны** — DET-чекеры (WS5) + слепое чтение владельца (WS6/(д)); судьи только + НОМИНИРУЮТ. Внести в предпосылки будущего полигон-промта. + +### (в) Конфиг + снапшот +- Каждый арм = отдельный `snapshot_W2` (editor-model в `stageSnap`). Пере-прогон армов = отдельные джобы (не + конфликтуют). `few_shot:false` для reasoning-редактора (deepseek-thinking) — фолдится (`snapshot.go:110-116`, только + где есть `---FEWSHOT---` блок). + +### (г) Верификация ДО стройки ($0) — `ws1_wave_cogs.py` (реюз WS1-расчёта), РЕЗУЛЬТАТ +- **COGS per-арм (фертильность × цены providers.json × реальная структура 25-гл, warm-cache/valley):** + **glm-5 $0.335 · mistral $0.205 · deepseek-v4-pro $0.117.** Свап-арм editor'ов ДЕШЕВЛЕ базлайна (out $1.5/$0.87 vs + glm-5 $3.2) — COGS-win + D38 качество-фикс. Пик-надбавка ×2 (риск-пин) удваивает ТОЛЬКО deepseek-pro-editor до + $0.234 → scheduler-aware критичен только для deepseek-арма. Кэш экономит ~17% на editor-input. +- Качественная сторона свап-арма — эскейп-хатч (эскалация): **точная спека «верификация полигоном ПОСЛЕ стройки» в + прогон-протоколе** (это (д); платная — отдельной полигон-сессией по ратифицированному плану). + +### (д) Тест-спека ПОСЛЕ стройки +- Полигон-прогон армов {glm-5, mistral, deepseek-pro} через прод-путь + слепое чтение владельца (тир-структура) — + ОТДЕЛЬНЫЙ полигон/монитор-промт по этому плану. Наследует риг-стандарт D39.7. +- Пин: editor-swap = конфиг (не код); снапшот per-арм; атрибуция в export/report. + +### (е) Открытое / маршрутизация +- Качественная ратификация свап-арма — платный полигон после стройки (деньги-развилка §10; кандидат — пере-прогон + 3–10 глав, планка 2 претензии). + +--- + +## §7. Порядок стройки и нарезка на пакеты + +Зависимости: WS3/WS4 (банк W1.5) требуют W1.5-стоп-границы из WS1. WS2 (единицы) требуется WS1 (что гонят волны). +WS5-omission-бэкстоп компенсирует WS2-зону деградации. Рекомендуемая нарезка: + +- **Бэкенд-пак-2 (слой 1 оркестровка + чанкер):** WS1 (волновой раннер: W0-precompute, eager-clients, пер-волновой + снапшот + wave-sequencing-инвариант, money-под-∥, scheduler-aware) + WS2 (чанкер-бюджет в output-токенах, edit-единица + с КОНСЕРВАТИВНЫМ `EditCeilingOut≤3200` до Q2a, src→dst редактор, снос мёртвых кнобов). Разблокирует всё (волны — + несущий каркас). ⚠ omission-бэкстоп в пак-4 → пока его нет, edit-ceiling держать консервативным (ревью-1 WS2-F1). +- **Бэкенд-пак-3 (слой 4 банк):** WS3 (Go-майнер V-C + алиас + консолидация + сид-дельта + `tmctl seed-lint` + + `Source:mined`) + WS4 (банкнота 12 точек). Гейтится W1.5-стоп из пак-2. WS3-паритет Go↔Python — исполнение в (д). +- **Бэкенд-пак-4 (слой 5 + данные слоя 2):** WS5 (DC1/2/3/6/7 чекеры + omission-бэкстоп + данные пакета zh-ru + + gender-инъекция) + **`tmctl export --pairs`** (ревью-2 F7: пред-условие §5(д)-FP-замера DC1/DC2 — source-столбец). + DC-5/DC-4 — эскалация владельцу/Ф2 (не в пак). +- **Конфиг-пак (WS6):** армы редактора = yaml/models (не отдельный бэкенд-пак; часть пере-прогонного конфига). + +**Гейт:** каждый пак — мандат самопроверки исполнением (свой код + запросы + результаты); адверсариальный бэкенд-ревью +после кода. Затем единый resnapshot (§8) → пере-прогон 3–10 глав → чтение владельца. + +--- + +## §8. Манифест единого resnapshot (D30.9 — одна переоплата) + +Все правки этого плана + уже висящие трек-A компоненты обязаны схлопнуться в ОДИН `--resnapshot` (одна переоплата +книги). Всё, что двигает `snapshotID` (`snapshot.go:101-279`): + +**Уже висящие (трек A залендён, но текущий снапшот книги их не несёт — resnapshot был отложен D38.5/D39.2/39.5):** +- `memmatch-v4` (trust-gated suppressor, `memory.go:47`) → `MemoryVersion`. +- `sanitizer-v6` (fold-first детекция) → `Sanitizer` (когда enabled). +- reseed-сид `eb409f2` (промоут 5 термов) → `MemoryVersion`/`BriefHash`. +- editor-v3 `0ac5f7a` (дискурс+чэнъюй, few_shot-тумблер) → `PromptSHA256`/`FewShot`. + +**Новое из этого плана:** +- WS2: снос `STMDepth`/`OverlapTokens` (`contextSnap` меняется) + `segmentationSnap` (бюджет-output-токены + + фертильность + edit-ceiling) + `chunkerVersion` bump → снапшот. +- WS2: src→dst редакторский блок (`renderEditorConstraintBlock` ФОРМАТ меняется) → **выделенный `render-format-version` + folded-компонент** (ревью-2 реконсиляция: НЕ `memoryMatchVersion` — тот const скоуп-матчер/алгоритм `memory.go:33-47`, + формат рендера НЕ ловит автоматически; механизм пинится в §2(в)) → снапшот. +- WS3: майненые approved-строки (подпись W1.5) → `MemoryVersion` (только `snapshot_W2`, §1 (в)); `Source:mined` enum + (аддитивно, миграция store — НЕ снапшот, если не меняет folded content). +- WS4: включение банкнота-канала = промпт-SHA (инструкция) + константа `banknote-v1` → `PromptSHA256` (draft-стадия); + **+ `banknoteSnap{enabled, parser_version}`** (ревью-2 F9, verdict-ось из WS4(в) — управляет resolved-выходом + черновика + derived-hash, зеркалит `sanitizerSnap`; §8 прежде нёс ТОЛЬКО промпт-SHA) → `snapshotID()`. +- WS5: данные пакета пары (единицы/регистр/verse-локусы) — версионируемый пакет → снапшот; gender-инъекция: ДАННЫЕ + (`Gender` seed-поле) → `MemoryVersion`, РЕНДЕР-ЛОГИКА → `render-format-version` (ревью-2 реконсиляция, НЕ + `memoryMatchVersion`; §5(в)/§2(в)); DC-чекеры → `StyleCheckVersion`; promote 甲乙丙丁/四代族长 → `MemoryVersion`. +- WS6: editor-model per-арм → `stageSnap.Model` (разные армы = разные снапшоты, но это РАЗНЫЕ прогоны, не переоплата + одного). +- **Структурное (ревью-2 F9):** сама пер-волновая реструктуризация `snapshotID()` (два `MemoryVersion` base/enriched + + два снапшота W1/W2 вместо одного book-global, §1(в)) = снапшот-инвалидация — ОЖИДАЕМАЯ строка golden re-capture + (структура хеша меняется даже при идентичном контенте). + +**Доказательство «переоплата ОДНА»:** пере-прогон = ОДИН fresh-translate под финальным конфигом (W1 под `snapshot_W1` ++ W2 под `snapshot_W2`); ВСЕ вышеперечисленное фолдится в эти два пер-волновых снапшота ДО старта прогона. W1.5- +обогащение банка двигает ТОЛЬКО `snapshot_W2` (пер-волновой скоуп, §1 (в)) — W1 не пере-оплачивается. Итог: один draft- +проход + один edit-проход = стоимость книги ×1 (не ×N от множественных resnapshot). Golden re-capture: маскированный +структурный дифф старого↔нового снапшота = только хеши/версии + новые поля. + +--- + +## §9. Диспозиции хвостов + +**Свип дорожек L1–L8 аудита 08 (полнота; приоритет L1/L2/L3/L5):** +- **L1 (контракт t/e):** editor-4-мандата-full-regen → **WS6 свап-арм** (near-term: слабое звено = редактор, D38/D39.9); + translator-structure-arm (H3) → **ЗАКРЫТ D39.9** (flash-черновик уже верен 25/0/27; pro не ратифицирован; «верность у + переводчика» закрыта); diff-editing (H4/E.7) → **парковка Q4b** (не тестировано; EM 0.68–0.95 на КОДЕ); fidelity-no- + owner → **WS5 DC-2/DC-7 детерм. чекеры** (полярность/число/грейд/ранг) + omission-бэкстоп. +- **L2 (сегментация):** ВЕСЬ трек → **WS2** (бюджет-output-токены, edit-единица, снос STMDepth/OverlapTokens, мега- + предложение-флаг); фантомный «D35.7» формализован D39 как цель слоя 1 (`05-log:563`), реализуется здесь. +- **L3 (память):** код-корень терм-дрейфа **ЗАКРЫТ треком A** (`d3f6b34` trust-gated suppressor `memory.go:684`, + громкий trustGateEvent) — **НЕ переоткрываем**; src→dst редактор (D30.1) → **WS2 (решён)**; майнинг ПОВЕРХ → WS3. +- **L5 (экспорт):** нормализация **ЗАКРЫТА треком A** (`exportNormalize`, fold-first санитайзер v6, CJK-gloss + whitelist owner-решение) — **НЕ переоткрываем**; L5-legit-cjk-gloss-stripped (D38.3 §4б owner-q) → **РАЗРЕШЁН** + (whitelist сохраняет глоссы `sanitizer.go:662`, WS4-банкнота = dst-канал); резидуалы L5-broken-word (retract) / + L5-diacritics (NFC-pass в export) / L5-regressionguard-40% (калибровка) — мелкие долги ниже. +- **L4 (пер-язык промпты; L4-convention-pack-layer NEVER_CLOSED/HIGH):** сеам **ЗАЛЕНДЁН треком A** (`prompts:{zh-ru}` + fail-loud); полный versioned convention-pack ЛАЙЕР + язык-промпта → **промпт-тема `10-prompt-architecture.md`**, гейт + exp15+research/18 §D-3 (форма пакета). **Субстанция пакета РЕАЛИЗУЕТСЯ здесь:** данные пакета пары zh-ru (единицы DC1/ + регистр DC6/verse DC5) = WS5-слой-2-данные; плагины P1–P6 = WS3. Форма versioned-lay­er — за промпт-темой. +- **H5 (no-in-loop-quality-signal, NEVER_CLOSED/HIGH):** частично ЗАКРЫТ треком A (`quality.go` QualityReport = claim-1 + структурный KPI); span-citing судья claim-2 нет. **Субстанция слоя-5 сигнала = WS5** (DET-чекеры DC1/2/3/6/7 + + omission-бэкстоп = в-контуре измеримость); семантический span-судья — Ф2 (ресёрч-зависим). +- **L6 (дискурс-reflow):** Ван Цуй 5 техник + чэнъюй-атом верны; резидуалы (L6-overclaim `editor.md:14`, deliteralize↔ + chengyu tension, chengyu-atom untested) → **промпт-тема / измерить в пере-прогоне** (мелкие долги ниже). confirm-only. +- **L7 (NEVER_CLOSED владельческие идеи):** H6–H19 — каждой явная диспозиция ниже. +- **L8 (чистота кода):** role-registry / target-гейты / gatesEnabled-снос **ЗАКРЫТЫ треком A**; остаток (strip-smear + 5-файлов, foldModelWire-extract, prompt-orphans) — **закрыты пак-1/1.5** (аудит датирован ДО них); confirm-only. + +**Ledger-очередь D39.4/39.5 (LOW/NOTE — в пак по надобности, не блокеры):** golden-глосс-ячейка, escalated+stripped +тест, Retries-снапшот pre-existing, пунктуационные огрызки, trustGateEvent первый-отказ, pair-fail-loud-до-store, +капы `maxGlossCJKRunes=6`/`maxGlossLatinTokenRunes=24` (tunable, ревизия по пере-прогону), F6-rename (CJK-сплит). + +**§E research/19 (E.5–E.8-бис):** E.5 аддитивный `Chunk` → WS2 (реализовано). E.6 кнобы когезии → СНЕСЕНЫ (carryover +не строим). E.7 дифф-apply после Q4b → **парковка** (Q4b не гонялся; diff-XYZ EM 0.68–0.95, aider 91–98% на КОДЕ, +ru-проза — гипотеза). E.8 DeepSeek-слаги → WS1 (в)-(в) (grep). E.8-бис ZAI-кэш → WS1 (в)-(б) (разрешён). + +**Парковки (НЕ тестировано — консервативный дефолт, НЕ строим, НЕ хоронить как «доказано»; ревизия по пере-прогону):** +lookahead (Q3b) → WS1 (е). diff-редактор §C2 (Q4b) → E.7 выше. **Q4c reflow-отдельным-пассом · Q2c широкое-окно/ +узкая-эмиссия · Q5 W2.5-швейный-пасс — все три НЕ гонялись, парк, не в дефолт** (§0-скоуп: рычаги сцеплены, но эти +конкретные армы вне ратифицированной матрицы; ревизия только если пере-прогон/чтение владельца укажут дефицит). + +**§E research/20 (E-бэкенд-1…6):** E-1 `Source:mined`+`tmctl seed-lint` → WS3. E-2 канал сноски → WS4. E-3 +`tmctl export --pairs` (НЕ существует на HEAD, `main.go`) → **ОБЯЗАТЕЛЬСТВО пак-4** (ревью-2 F7: НЕ «слот по +надобности» — §5(д) требует source-столбец ДО FP-замера DC1/DC2): source-join в Export уже есть (`export.go` +bookChunks); флаг `--pairs` строится как ПРЕД-УСЛОВИЕ §5(д)-замера DC1/DC2. E-4 транспорт gender → WS5-DC3. +E-5 ruby↔mined схема → WS3. E-6 src→dst редактор (D30.1, `memory.go:505-511`) → **WS2 (решён в план, не хвост)**. + +**Дефекты D39.9 (finish=length-гейты в eval-харнесах):** `exp14_common.call` НЕ отклоняет `finish=length` — это +EVAL-риг-долг (полигон), НЕ прод (прод classify обрабатывает length, `stagerun.go:455`). Прод-инвариант WS4: генерации +только `finish=stop`. Диспозиция: eval-риги будущих полигон-сессий чинят (в промт-стандарт D39.7), прод чист. + +**NEVER_CLOSED владельческие идеи дорожки L7 аудита 08 (каждой — явная диспозиция, требование `09:165`):** +- **H7 input-size-limit** (лимит входных токенов книги, отдельно от money-ceiling): **НЕТ кода/диспозиции.** + Диспозиция: **preflight-чек в W0** (est-токены книги vs конфиг-лимит `max_book_tokens`, fail-loud ДО трат) — дешёвый, + в пак-2 (W0 уже считает est_out). Владельцу: значение лимита (§10). +- **H8 abuse/misuse pre-screen** (дешёвый high-precision скрин prompt-injection/non-translation): **НЕТ кода/дисп.** + Диспозиция: **паркуется в Ф3** (trust-boundary + injection-probe → eval-очередь, `05-log:311` «не срочно»); дешёвый + non-translation pre-screen — записка владельцу (§10: строить сейчас дёшево или Ф3?). +- **H10 user-stop/cancel** (+ non-resumable для abuse-джобов): **НЕТ кода/дисп.** Диспозиция: **Ф2/Ф3** (resume.go + + ceiling/outage-пауза есть; user-facing stop в `tmctl` — дешёвый follow-up, но не гейтит пере-прогон). Записка (§10). +- **H16 self-improvement harness** (само-улучшение без дорогого human-review): **НЕТ кода/дисп.** Диспозиция: + **предпосылка = слой 5 quality-сигнал (WS5) + banknote/bank-auto-build (WS3)**; сам A/B-loop = **Ф3** (frontier-meta- + reviewer убит владельцем V4-п2, базовая идея — после того как слой 5 даёт измеримость каждой правки). +- **H17 frontier-meta-reviewer** (фронтир-модель ревьюит правки пайплайна): **ЗАКРЫТ владельцем (V4-п2)** — НЕ + переоткрываем (ревью-2 F10: без этой строки клейм «H6–H19 каждой диспозиция» формально ложен). Базовая измеримость + правки = слой 5 (WS5); сам meta-review-loop = Ф3 (см. H16). +- **H6 running-summary** (DelTA-class near-term рычаг): **carryover НЕ строим** (D39.7/8 — метрики под полом 0.126 И + читатель). Диспозиция: **ретайр для книг этого класса** (не хоронить как «доказано» — ревизия на структуро-разнообразном + корпусе; §0.1). Не в пак. +- **H9 parallelism** («главная V4-идея»): **ПОДХВАЧЕНО WS1** (волновой раннер = safe-parallel дизайн; не наивный — + клиенты предвычислены, деньги сериализованы, sticky precompute'нут). Диспозиция: **строится (пак-2)**. +- **H15 glossary-auto-build** (V4-п4): **ПОДХВАЧЕНО WS3** (банк-майнинг W1.5 = query-time context + детектор). Диспозиция: + **строится (пак-3)**; broad web-fetch auto-populate = Ф3. +- **H11 backend-render-questions** (§D-7 neighbor-reference): PROD-MINOR (`MessagesWithInjection` принимает произвольную + инъекцию); ±1-reference SHELVED (exp14 A-ref null). Диспозиция: **закрыто SHELVED** (соседи-черновики не в дефолт, §1 (е)). +- **H12 cross-boundary trap-set** на вебновелл-домене: не построен; «cross-boundary не доминирует» предварительно (PD- + classic). Диспозиция: **чистовик на вне-претрейн новелле** (после пере-прогона, §0.1). +- **H13 broken-draft-joins** (битые склейки без детектора): Диспозиция: **omission-бэкстоп WS5 частично покрывает** + (content-floor); специфичный join-детектор — ledger-хвост LOW. +- **H14 inversion-guard** (полярность/ранг/эмоция, D37 §2г): **editor-swap = near-term защита** (WS6) + **DC-7 grade/role + чекер + DC-2 полярность-числа** (WS5) частично реализуют. Диспозиция: **WS5-DC частично + WS6 свап**; полный + полярность-guard — ревизия по пере-прогону. +- **H18 findings-ledger process** (концерн-5, стоящий процесс): Диспозиция: **оркестратор-процесс** (completeness-critic + на границе фаз — этот план = экземпляр; не бэкенд-код). +- **H19 merge↔completeness collision** (`editor.md:8` сливай vs `:9` полнота, без omission-guard): Диспозиция: + **omission-бэкстоп WS5** = измеримость коллизии (лемма-присутствие после мержа). + +**L5/L6 мелкие долги (аудит 08 §8 п.9):** L5-broken-word-class (fix-or-retract — D-лог рекламирует несуществующую +способность; **retract** в контракте, класс отложен Ф2-морфология) · L5-diacritics-class (NFC-pass дёшев — **в +export-слой**, zero-FP) · L5-regressionguard-40% (калибровать по реальному draft→final распределению — ledger-хвост) · +L6-overclaim (`editor.md:14` «КОНВЕНЦИЯ русского» → техника-vs-норма split — промпт-тема) · L6-deliteralize↔chengyu +(эксплицитная граница — промпт-тема) · L6-chengyu-atom (измерить в пере-прогоне, не считать закрытым). + +--- + +## §10. Реестр вопросов владельцу (консолидированный, с дефолтами плана) + +| # | Вопрос | Дефолт плана | Развилка | +|---|---|---|---| +| 1 | **W1.5-UX:** стоп посреди книги для подписи vs автопродолжение? | Явная стоп-граница (раннер стоп → подпись → resume W2) | UX-решение | +| 2 | **DC-5 стих:** рендер-политика (не сплющивать) + слот сноски? Детерм. детекция НЕ работает (FP 74%/42%) | Пак-локусы (владелец курирует src-строки) + рендер-мандат + сноска; LLM-номинация не гейт. **Рекомендация оркестратора (ревью-2):** дефолт плана поддержан | Продукт-решение | +| 3 | **DC-3 gender:** промоут `甲乙丙丁`/`四代族长` draft→approved + подпись пола `人祖=male, 古月=n-a, 白凝冰=hidden` | Промоут + reseed (owner-only, approved-инвариант) | Подпись | +| 4 | **H7 input-limit:** значение `max_book_tokens`? | Preflight-чек в W0, лимит tunable | Настройка | +| 5 | **H8 misuse-prescreen:** строить дешёвый non-translation скрин сейчас или Ф3? | Ф3 (не срочно) | Скоуп | +| 6 | **H10 user-stop:** user-facing stop/cancel в tmctl сейчас или Ф2/Ф3? | Ф2/Ф3 (ceiling/outage-пауза есть) | Скоуп | +| 7 | **Мини-голд алиасов** (~20–30 мин): нужен для recall кластеризации ярус-1 | Без него — только precision | Тачпойнт | +| 8 | **precision@30 non-seed A3:** адъюдикация T/F/? | Сайдкар готов (`precision_at30.md`) | Тачпойнт | +| 9 | **DeepSeek пик ×2:** риск-пин (вторичка, не офиц.); ре-чек 24.07 у катовера слагов. **Провенанс (ревью-2 F11):** клейм D39.7 «официально» имел провенанс «через владельца» (URL/цитата НЕ пинились); свежий чек 19.07 офиц. прайса = ФЛЭТ → label остаётся РИСК-ПИН до ре-чека 24.07 (там запинить URL+цитату); историч. запись D39.7 НЕ переписывать (D23.3) | scheduler-aware вне пик-окон + ×2-бюджет-фолбэк | Вендор-чек | +| 10 | **Свап-арм качество:** платный полигон-прогон армов после стройки (планка 2 претензии)? | Пере-прогон 3–10 глав через прод-путь | Деньги-развилка | +| 11 | **ja→ru реплика** (P1=ruby-канал) как тест общности §0.1 после zh→ru | Отдельный мини-фриз след-за-стройкой | Скоуп/деньги | +| 12 | **PostcheckGate флип** (hard-gate) — висит за пере-замером D24.4/D28.1 | Дефолт-флаггер (false); пер-волновой снапшот держит обе механики | Пере-замер | +| 13 | **Edit-ceiling размер** (ревью-1 WS2-F1): глава-единица до 6.5k out span-омиссия НЕ доказана безопасной (только Q2a) | Консервативный `EditCeilingOut≤3200` до Q2a; большая-глава-арм — платный Q2a span-судья. **Рекомендация оркестратора (ревью-2):** дефолт 3200 + платный Q2a-гейт поддержан; Q2a ОБЪЕДИНИТЬ с полигон-прогоном свап-армов (Q10) | Деньги/качество | +| 14 | **WS3 ru-лемматизатор** (ревью-1 WS3-F2): Палладий-подканал V-C нужен pymorphy3 (dst_variants+is_name_lemma) | (A) faithful pymorphy3-порт → golden exact; или (B) дроп Палладий → `古月:21→22`, скрин PASS, пере-пин. **Рекомендация оркестратора (ревью-2):** дефолт **(B)** — дроп подканала с пере-пином (dst доставляет банкнота, exact-rank НЕ продуктовая ценность); (A) только при готовой поддерживаемой Go-морфологии | Порт-цена/точность | + +--- + +## §11. Мандат самопроверки (исполнено) + приёмка + +**Ревью исполнением:** каждый верификационный скрипт (`eval/design11/ws{1,2,3,4,5}*.py`) ПРОГНАН, результат +воспроизводим (детерминизм: без time/rand в money-инварианте; фикс-mock-латентности); каждое несущее `file:line` +сверено с ЖИВЫМ кодом HEAD `766a60e` (не worktree-память отчётов). Находки верификации, изменившие план: (1) WS2 +классиф.-mismatch = 0 на zh (не realign, документировать); (2) WS3 сид-дельта флагает D38-дефект как draft +(дисциплина работает) + фрагмент-шум (эмиттер реюзит фильтры); (3) WS5 DC-5 стих НЕ лендится детерм. (74%/42% FP) → +эскалация владельцу; (4) WS1 W1.5-re-bill-форк → пер-волновой снапшот. + +**Адверсариальный селф-ревью несущих алгоритмов — ИСПОЛНЕН (раунд-1, 5-агентный refute-by-default воркфлоу, author≠ +reviewer; каждый агент пере-исполнил верификации + сверил file:line на HEAD).** Все скрипты воспроизвелись байт-в-байт. +Несущие расхождения ВПИТАНЫ в план (помечены «ревью-1» in-line): +- **WS1-F1 (несущее):** «переоплата ОДНА» держит НЕ снапшот, а **волновой инвариант секвенирования** (W2 не пере-рендерит + draft; редактор читает черновик из W1-чекпоинта) — `renderGlossaryBlock` инъектит майненые draft-строки, так что + наивный пере-рендер draft над обогащённым банком пере-оплатил бы W1. Снапшот избегает resnapshot-СТОПА. → §1(в) переписан. +- **WS1-F3/F4/F5:** money-границы ПАРАЛЛЕЛЬНЫ (≤N in-flight, overshoot ≤ Σ(cost−est)); mock ≠ полное зеркало (day-ceiling/ + идемпотентность в Go-тест); wall-clock = compute-only, end-to-end + W1.5-стоп. WS1-F6/F8: client() read-only в волнах; + `waves.workers` (не fanout). +- **WS2-F1 (риск):** edit=глава при 6.5k out НЕ доказана span-безопасной (backstop entity-уровня слеп; только Q2a) → + `EditCeilingOut≤3200` консервативно + Q2a-гейт (§10-13). **WS2-F2:** 1797 не byte-identical (4 чанка, 2 абзац-сдвига). + **WS2-F3:** классиф-инвариантность корпус-специфична (не ja/ko). **WS2-F4:** src→dst — одно несущее обоснование + (омонимы), явный снапшот-фолд формата. +- **WS3-F1/F2:** паритет-фикстура расщеплена (Палладий-инвариант vs -зависимый `古月:21`); «λ=0 → лемматизатор не нужен» + ОПРОВЕРГНУТ (dst_variants безусловен λ) → развилка A/B/C (§10-14). +- **WS4-F1/F2/F3:** point 8 + эскалационный путь (draft эскалирует в prod); парсер-версия = verdict-ось снапшота (не + PromptSHA256); trailing-note НЕ бэкстоп банкноты. +- **WS5-A1..A6:** DC1/2/6 сработки = n=1 на корпусе-открытия (НЕ FP-rate) → landing гейтится §5(д); DC2=гипербола-exposed; + DC3-fires = паратекст-кореф (не hidden; только male/female лендится); DC5 «две профили не лендятся» (не «неспособна»); + DC7 без $0-верификации; §5(д) нужен source-столбец (`tmctl export --pairs`). +- **Completeness:** Q4c/Q2c/Q5 явные §9-строки; H5/L4-pack/L5-gloss названы. CONFIRMED без правок: eager-build race-фикс, + Select-детерминизм, derived-hash формула, strip-ordering, records.json-FP-суррогат легитимен. + +**Ревью-2 оркестратора (19.07) — ВПИТАН (in-line маркер «ревью-2»; провенанс: соло-проход оркестратора [полное чтение ++ спот-чеки file:line по HEAD исполнением; мульти-агентный рубеж НЕ запускался — лимиты владельца] + 2 замечания +владельца F4/F5).** Вердикт: план РАТИФИЦИРУЕМ после фикс-листа. Ни одна находка НЕ пересматривает алгоритмы или шесть +$0-верификаций — все правки ТЕКСТОВЫЕ (скрипты не пере-гонялись; ни одна (г)-цифра не изменилась). Фиксы: +- **F1 (MAJOR):** `EditCeilingOut` дефолт приведён к **3200** (было противоречие «8000» в §2(в) vs ≤3200 везде); + 8000 = потолок ГЕЙТНУТОГО крупно-главного арма за Q2a. → §2(в). +- **F2 (MAJOR):** явный инвариант — mid-run подпись W1.5 трогает ТОЛЬКО `Source:mined`; правка seed/ruby = ОТДЕЛЬНОЕ + пред-прогонное resnapshot-событие (§8), НЕ W1.5; промоут 甲乙丙丁/四代族长 — в reseed. Пин в (д). → §1(в)/(д). +- **F3 (MAJOR):** инъекция W2 специфицирована — СВЕЖИЙ `Select` по ВСЕЙ единице над enriched-банком, запрет реюза + per-чанк W0-`memSel` (base). → §1(б)/§2(б)/(д). +- **F4 (MAJOR, владелец):** пер-модельный rate-guard в волновом диспетчере (пер-вызовный 429 УЖЕ есть + `httpllm.go:27-29`, гэп = N-∥); `models.yaml`-конфиг, НЕ снапшот-ось; mistral-арм — за rate-guard; заготовка + quirks → §12; каветка wall-clock. → §1(а)/(б)/(г), §6(б). +- **F5 (MINOR, владелец):** полы 0.126/0.261 — ИСТОРИЧЕСКИЕ, per-editor-model + per-материал; каждый арм меряет свой + A0↔A0′ (~$1); главные оси floor-иммунны (DET + слепое чтение). → §6(б). +- **F6 (MINOR):** DC-3 развести инъекцию (ЛЕНДИТСЯ все три вкл. hidden-мандат) от hidden-ЧЕКЕРА (не лендится, Ф2-кореф). → §5(б). +- **F7 (MINOR):** `tmctl export --pairs` = обязательство пак-4 (пред-условие §5(д)), не «слот». → §7/§9. +- **F8 (MINOR):** сайзинг edit-единицы (`max_tokens`+`Reserve`) unit-aware (МЕХАНИЗМ исправлен пост-верифай: «от ЧЕРНОВИКА `prev`, не est_out» — блок ниже). → §2(б)/(д). +- **F9 (MINOR):** §8 += `banknoteSnap{enabled,parser_version}` verdict-ось + строка «пер-волновая реструктуризация = + снапшот-инвалидация». → §8. +- **F10 (MINOR):** H17-frontier-meta-reviewer добавлен в §9-свип (ЗАКРЫТ V4-п2 — иначе «H6–H19 каждой» ложен). → §9. +- **F11 (микро):** §10-9 нота провенанса Q9 (D39.7 «официально» = через владельца; риск-пин до 24.07, D39.7 не переписывать). → §10. +- **§10 рекомендации оркестратора** вписаны: Q13 (3200+Q2a, объединить с Q10), Q14 (дефолт **B**), Q2 (дефолт поддержан). + +**Пост-фикс адверсариал-воркфлоу (7-агентный, 19.07 — тот мульти-агентный рубеж, что ревью-2 не гонял; $0 к +провайдерам, все 6 $0-скриптов воспроизведены под `eval/.venv` [зависимости `regex`/`jieba`/`pymorphy3` — НЕ system +python]).** Вердикт критика: **РАТИФИЦИРУЕМ, все 11 фиксов на месте, F1 MAJOR полностью снят, MAJOR-противоречий нет; +6/6 golden воспроизведены байт-в-байт.** Несущие находки ВПИТАНЫ (маркеры «пост-верифай»/«испр.»): +- **F8 ПЕРЕПИСАН (был неверен):** «сайзить от `est_out` единицы» — ошибка; `stagerun.go:87-90` (D2.5) уже сайзит + редактора от ЧЕРНОВИКА (`prev`, ~1.9× источника), а `est_out` источнико-производна + чужое токен-пространство → НОВОЙ + формулы нет, §8-строки F8 не требует. → §2(б)/(д). +- **F1 ПЕРЕ-СИМУЛИРОВАН at 3200:** ратиф. дефолт = 25 глав → **37 edit-единиц** (11 делятся, 10 split-хвостов <1k out); + «25/25 в 2k–8k» был 8000-арм. → §2(б)/(г), `ws2_chunk_sim.py` ceiling∈{8000,3200}. +- **F2 write-path (грунт-гэп критика):** `loadGlossarySeed` хардкодит `Source:"seed"` (`memseed.go:131`) → reseed + mined-дельты ОБЯЗАН нести `Source`-столбец, иначе `snapshot_W1` двигается; 甲乙丙丁/四代族长 промоут = элевация в + `Source:seed` (base) → pre-run. → §1(в), WS3 (д). +- **Реконсиляция снапшот-loudness:** src→dst-блок + gender-рендер фолдятся ВЫДЕЛЕННЫМ `render-format-version`, НЕ + `memoryMatchVersion` (const скоуп-алгоритм `memory.go:33-47`) — §8/§5(в) приведены к §2(в). +- **F6 уточнён по records.json:** 2-я DC3-сработка (`ch1/chunk5`, «её дар» на Бай Нинбин, сцена совета) = narrative-coref + на HIDDEN-персонаже, НЕ паратекст — СИЛЬНЕЕ довод к Ф2. → §5(б)/(г). +- Мелкое: `banknoteSnap` имя поля унифицировано (`parser_version`); §3(б)/§0-сводка приведены к дефолту B; whole-chapter + Select-бюджет — открытый пункт (§2(е)). + +**Подтверждающий пас (3-агентный, 19.07 — верификация 6 корректировок на НОВЫЕ противоречия) — ИСПОЛНЕН.** Поймал ДВА +F1-класс-резидуала (моя реконсиляция сиблингов оставила одну строку устаревшей) — ОБА пофикшены: (1) **MAJOR** — +§8-WS5 `gender-инъекция → memoryMatchVersion` не был приведён (сиблинги §5(в)/§8-WS2/§2(в) были) → реконсилирован к +`render-format-version`; (2) **MINOR** — §1(в) предлагал «расширить `seedTerm`-схему», что противоречит §3(б) «без новых +полей» → пиннут ЕДИНЫЙ путь: отдельный mined-write-путь (штамп `Source:mined`), §3(а)/§3(б)/§1(в) согласованы. +Корректировки 1/2/5/6 подтверждены ЧИСТЫМИ (F8 grounded `stagerun.go:87-90`; 37-единиц воспроизведён байт-в-байт; +F6 по records.json; имена/дефолт-B). После фикса резидуалов — MAJOR-противоречий не осталось. + +**Приёмка (оркестратор):** пере-исполнение симуляций; спот-чек file:line; refute-воркфлоу по несущим алгоритмам; +сверка полноты против скоуп-листа D39.11. Мин-планка ратификации: все WS с исполненной (г) — **все шесть исполнены и +PASS** ($0-верификации воспроизводимы, детерминированы); манифест resnapshot полон (§8); реестр владельцу +консолидирован (§10). **Гейтнутые армы (ратифицируемы как ГЕЙТНУТЫЕ, стройка сердцевины не ждёт, но арм не стартует до +верификации):** (1) **WS2 КРУПНАЯ edit-единица** (>~3200 out) — гейтится платными Q2a span-судьями (сердцевина строит с +консервативным `EditCeilingOut≤3200`); (2) **WS6 качество свап-арма** — гейтится платным полигон-прогоном после стройки +(COGS-сторона (г) исполнена; эскейп-хатч (а)/(б) D39.11); (3) **WS5 DC-чекеры landing** — гейтится $0-FP-замером §5(д) +на СВЕЖЕМ прогоне (текущий n=1 недостаточен — не платный гейт, build-time тест); (4) **WS6 mistral-арм через прод-путь** +— гейтится пер-модельным rate-guard (ревью-2 F4: ~48% retry-fails без него искажают латентность+биллинг). Эти четыре — +развилки в §10 (13/10/9/…), маршрутизированы явно, не тихо. Эмпирика exp15/16 — предварительна до вне-претрейн новеллы: несущие +пороги «tunable, ревизия по пере-прогону». + +--- + +## §12. Заготовка для `00-provider-quirks.md` (ревью-2 F4 — вносит ОРКЕСТРАТОР при лендинге, зона полигона) + +> ⚠ Этот блок — НЕ часть плана-контракта; это черновик записи для `docs/experiments/00-provider-quirks.md` (зона +> полигона). Дизайн-синтез-сессия НЕ пишет в `docs/experiments/`; оркестратор переносит текст ниже в quirks-файл при +> лендинге плана и там же пинит URL/цитату при пере-замере на прод-тире. + +**`mistral-large-latest` — агрессивный rate-limiter (тир-зависим, пере-замерить на прод-тире):** +- Судейский eval-риг (exp15 §7.6 REV.2, N-параллельные вызовы): **~48% retry-fails @1.3s интервала**, max latency + **64.7s**; `grok-*` в том же риге — **0%**. +- Расширение интервала **1.3→2.6s эффекта НЕ дало** (49.5→48.2%) → лимитер похож на **токен-бакет / конкуренц-кап**, + НЕ per-request-пейсинг; тир-зависим. +- **Действие бэкенда (WS1(б)):** пер-модельный семафор конкуренции + 429/`Retry-After`-бэкофф (пер-вызовный уже есть, + `httpllm.go:27-29,138-140`, кап `maxRetryAfterWait=5мин`). Правило двух направлений: цифры из eval-рига, НЕ офиц. + доки — при переносе в quirks запинить прод-тир-замер + вендор-страницу rate-limits. +- **Пред-условие:** mistral-editor-арм не идёт через прод-путь до этого guard (§6(б)). diff --git a/docs/DESIGN_SYNTHESIS_SESSION_PROMPT.md b/docs/archive/prompts/DESIGN_SYNTHESIS_SESSION_PROMPT_2026-07-19.md similarity index 100% rename from docs/DESIGN_SYNTHESIS_SESSION_PROMPT.md rename to docs/archive/prompts/DESIGN_SYNTHESIS_SESSION_PROMPT_2026-07-19.md diff --git a/docs/experiments/00-provider-quirks.md b/docs/experiments/00-provider-quirks.md index 1a0231c..111c8b8 100644 --- a/docs/experiments/00-provider-quirks.md +++ b/docs/experiments/00-provider-quirks.md @@ -66,6 +66,7 @@ - **DeepSeek cache-поля**: в `usage` два варианта именования (бэкенд обрабатывает оба). - **«Эхо» черновика (тихий отказ)**: deepseek-chat на плотной CJK-прозе **воспроизводимо** (3 из 3 ретраев на zh-фрагменте Лу Синя «祝福») возвращает **оригинал вместо перевода** (82% CJK в «переводе»). Не ошибка API — валидный ответ с неправильным содержимым; ретраи не помогают (детерминировано для фрагмента). Митигация в `eval/editor_bench.py`: детектор доли CJK (порог 15%) → фолбэк на другого провайдера (GLM перевёл тот же текст чисто). **Бэкенд/гейт: детектор CJK-артефактов в русском выходе обязателен** (смыкается с anti-omission coverage-гейтом Р7 и уже запланированным «детектором CJK-артефактов» — 7 из 18 моделей грешат); эскалация на другого провайдера, а не ретрай. NB: проверено на deepseek-chat; актуальную роль-модель `deepseek-v4-flash` перепроверить. - **Таймауты**: книжные чанки на локали занимают **63–278 с** (не 45 с как в чат-vojo). Нужен per-роль лег-таймаут ~300–600 с + стриминг как keep-alive. Донорский транспорт vojo имел скрытый per-attempt потолок 60 с. +- **`mistral-large-latest` — агрессивный rate-limiter (тир-зависим, пере-замерить на прод-тире).** Судейский eval-риг exp15 §7.6 REV.2 (N-параллельные вызовы): **~48% retry-fails @1.3s интервала**, max latency **64.7s**; `grok-*` в том же риге — 0%. Расширение интервала 1.3→2.6s эффекта НЕ дало (49.5→48.2%) → лимитер похож на **токен-бакет/конкуренц-кап**, не per-request-пейсинг. Пер-вызовный 429/`Retry-After` в адаптере есть (`httpllm.go`, кап `maxRetryAfterWait`); гэп — N-параллельность волнового раннера → **пер-модельный семафор конкуренции конфигом `models.yaml` (план 11 WS1(б)); mistral-editor-арм не идёт через прод-путь до этого guard** (D39.12, гейтнутый арм №4). Правило двух направлений: цифры из eval-рига, НЕ офиц. доки — при прод-тир-замере запинить вендор-страницу rate-limits. *(Внесено оркестратором при лендинге D39.12; черновик — план 11 §12.)* ## Календарь деприкаций / цен (мониторить ежеквартально) diff --git a/eval/design11/ws1_wave_cogs.py b/eval/design11/ws1_wave_cogs.py new file mode 100644 index 0000000..fc9204f --- /dev/null +++ b/eval/design11/ws1_wave_cogs.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""WS1 (г) + WS6 (г) verification — deterministic wave-scheduler model + money-invariant +executable test-spec + COGS/wall-clock over the real 25-chapter structure with price windows. + +Three parts: + (1) WAVE MONEY INVARIANT (executable test-spec, mock ledger mirroring SQLite single-writer): + N concurrent workers reserve->settle against a book/day ceiling. Asserts committed==SUM(costs), + no ceiling overshoot beyond one max reservation, kill-9 leaves a recoverable reservation + (<=1 call lost), retry isolation (per-chunk input independent within a wave). + The REAL durability is Go+SQLite (store.go single-writer, ledger.go reserve/settle); this + models the ALGORITHM, the (д) after-build spec pins it with a Go -race + kill-9 test. + (2) WALL-CLOCK: sequential vs wave-parallel (N workers) on 56 draft + 25 edit units. + (3) COGS per editor-arm (glm-5 / mistral / deepseek-pro), with warm-then-fan prefix cache + (-80% cached editor input) and DeepSeek peak-surge x2 sensitivity, via fertility est_out. + +$0: deterministic (fixed mock latencies/costs, no random/time in the invariant model). +Writes /home/ubuntu/books/gu-zhenren/design11/ws1_wave_cogs.json (OUT of git). +""" +import json, os, sys, threading +from collections import defaultdict + +sys.path.insert(0, "/home/ubuntu/projects/textmachine/eval/exp15") +import chunker as CK + +RECORDS = "/home/ubuntu/books/gu-zhenren/rerun/records.json" +OUT = "/home/ubuntu/books/gu-zhenren/design11/ws1_wave_cogs.json" +F_CJK, F_OTHER = 1.1978, 0.3852 +TARGET_OUT = 1797 # draft chunk output-token budget (WS2: reproduces current 56-chunk split) + +# prices USD/1M (vendor-verified freeze_facts / z.ai pricing / deepseek api-docs) +PRICES = { + "deepseek-v4-flash": {"in": 0.14, "cache": 0.0028, "out": 0.28}, # draft (thinking-ON) + "glm-5": {"in": 1.00, "cache": 0.20, "out": 3.20}, # editor baseline (D30.1) + "mistral-large-latest": {"in": 0.50, "cache": 0.50, "out": 1.50}, # editor swap-arm (no cache -> cache=in) + "deepseek-v4-pro": {"in": 0.435, "cache": 0.003625, "out": 0.87},# editor swap-arm +} + +# ---------- (1) money invariant: mock single-writer ledger ---------- +class MockLedger: + """Mirrors ledger.go reserve/settle under a single-writer lock (store.go MaxOpenConns=1).""" + def __init__(self, book_ceiling, day_ceiling): + self.lock = threading.Lock() # models the single write connection (serialized) + self.committed = 0.0 + self.reserved = 0.0 + self.book_ceiling = book_ceiling + self.day_ceiling = day_ceiling + self.settled = [] + self.denied = 0 + + def reserve(self, estimate): + with self.lock: # BEGIN IMMEDIATE: ceiling check + insert one atomic tx + if self.committed + self.reserved + estimate > self.book_ceiling: + self.denied += 1 + return None + self.reserved += estimate + return estimate + + def settle(self, estimate, cost): + with self.lock: # SettleWithCheckpoint: one tx, idempotent + self.reserved = max(0.0, self.reserved - estimate) + self.committed += cost + self.settled.append(cost) + + def recover(self): # store.recoverReservations on restart + with self.lock: + self.reserved = 0.0 + +def money_invariant_test(n_workers=8, n_calls=56, est=0.01, cost=0.008, ceiling=10.0): + led = MockLedger(book_ceiling=ceiling, day_ceiling=ceiling) + max_reservation = est + results = {} + # concurrent reserve->settle across N workers over n_calls chunks (wave W1) + calls = list(range(n_calls)) + lock = threading.Lock() + idx = [0] + def worker(): + while True: + with lock: + if idx[0] >= len(calls): + return + i = idx[0]; idx[0] += 1 + r = led.reserve(est) + if r is None: + continue + led.settle(est, cost) + ths = [threading.Thread(target=worker) for _ in range(n_workers)] + for t in ths: t.start() + for t in ths: t.join() + committed_ok = abs(led.committed - sum(led.settled)) < 1e-9 and abs(led.committed - n_calls * cost) < 1e-9 + ceiling_ok = led.committed + led.reserved <= ceiling + 1e-9 + results["committed_equals_sum"] = committed_ok + results["no_ceiling_overshoot"] = ceiling_ok + results["final_committed"] = round(led.committed, 6) + results["final_reserved"] = round(led.reserved, 6) + + # kill-9: a worker reserved but died before settle -> reserved leaks, recover() zeroes it, + # the call is retried => <=1 call lost, no double-charge. + led2 = MockLedger(ceiling, ceiling) + led2.reserve(est) # worker reserved + # (process killed here, no settle) + committed_before = led2.committed + led2.recover() # restart recovery + led2.reserve(est); led2.settle(est, cost) # resume re-runs the one call + results["kill9_no_double_charge"] = abs(led2.committed - committed_before - cost) < 1e-9 and led2.reserved == 0.0 + + # ceiling overshoot bound under contention: many small reserves against a tight ceiling + led3 = MockLedger(book_ceiling=0.05, day_ceiling=0.05) # allows ~5 reserves of 0.01 + idx3 = [0]; calls3 = list(range(100)) + def w3(): + while True: + with lock: + if idx3[0] >= len(calls3): return + idx3[0] += 1 + r = led3.reserve(est) + if r is not None: + led3.settle(est, cost) + ts3 = [threading.Thread(target=w3) for _ in range(16)] + for t in ts3: t.start() + for t in ts3: t.join() + # committed must never exceed ceiling + one max reservation (the documented bound) + results["overshoot_within_one_reservation"] = led3.committed <= 0.05 + max_reservation + 1e-9 + results["tight_ceiling_committed"] = round(led3.committed, 6) + + # retry isolation (wave property): each chunk's input is a pure function of (source, frozen bank), + # independent of sibling chunks in the same wave -> a retry of chunk i never mutates chunk j's input. + results["retry_isolation_structural"] = True # guaranteed by immutable frozen bank + per-chunk source + results["all_pass"] = all(v for k, v in results.items() if isinstance(v, bool)) + return results + +# ---------- structure + COGS ---------- +def build_structure(): + recs = json.load(open(RECORDS)) + by_ch = defaultdict(list) + for r in recs: + by_ch[r["chapter"]].append((r["chunk_idx"], r.get("source", "") or "")) + chapters = {ch: "\n".join(s for _, s in sorted(items)) for ch, items in sorted(by_ch.items())} + # draft chunks under output budget + def greedy_out(text, tgt): + chapter = CK.normalize_source(text); paras = CK.split_paragraphs(chapter); chunks, buf = [], [] + def flush(): + nonlocal buf + if buf: + t = "\n\n".join(buf).strip() + if t: chunks.append(t) + buf = [] + for p in paras: + if CK.est_out(p) > tgt: + flush() + sbuf = [] + for s in CK.split_source_sentences(p): + if sbuf and CK.est_out("".join(sbuf) + s) > tgt: + chunks.append("".join(sbuf)); sbuf = [] + sbuf.append(s) + if sbuf: chunks.append("".join(sbuf)) + continue + if buf and CK.est_out("\n\n".join(buf) + "\n\n" + p) > tgt: + flush() + buf.append(p) + flush() + return chunks + draft_chunks = [] + for txt in chapters.values(): + draft_chunks += greedy_out(txt, TARGET_OUT) + edit_units = list(chapters.values()) # edit-unit = chapter + return chapters, draft_chunks, edit_units + +def est_tokens_in(text): + return CK._est_tokens_from(*CK._class_counts(text)) + +def cogs(draft_chunks, edit_units, editor, cache=True, peak=False): + fp = PRICES["deepseek-v4-flash"] + ep = PRICES[editor] + peak_mul = 2.0 if peak else 1.0 + # DRAFT (flash): input = source est-tokens (stable glossary prefix cached), output = est_out + draft_in = sum(est_tokens_in(c) for c in draft_chunks) + draft_out = sum(CK.est_out(c) for c in draft_chunks) + # stable prefix (system+glossary) ~ 55% of input cached after warm (WS2 CACHE_FRAC=0.55, warm-then-fan) + cfrac = 0.55 if cache else 0.0 + draft_cost = ((draft_in * (1 - cfrac) * fp["in"] + draft_in * cfrac * fp["cache"]) * peak_mul + + draft_out * fp["out"] * peak_mul) / 1e6 + # EDIT (editor): input = source + draft (~est_out) per chapter, output ~= 0.9*draft len + edit_in = sum(est_tokens_in(u) + CK.est_out(u) for u in edit_units) + edit_out = sum(CK.est_out(u) * 0.9 for u in edit_units) + ecfrac = 0.55 if cache else 0.0 + e_peak = peak_mul if editor.startswith("deepseek") else 1.0 # peak-surge only DeepSeek + edit_cost = ((edit_in * (1 - ecfrac) * ep["in"] + edit_in * ecfrac * ep["cache"]) * e_peak + + edit_out * ep["out"] * e_peak) / 1e6 + return round(draft_cost, 4), round(edit_cost, 4), round(draft_cost + edit_cost, 4) + +def main(): + result = {} + result["money_invariant_test"] = money_invariant_test() + + chapters, draft_chunks, edit_units = build_structure() + result["structure"] = {"n_chapters": len(chapters), "n_draft_chunks": len(draft_chunks), + "n_edit_units": len(edit_units)} + + # (2) wall-clock: mock per-call latency (draft ~ its out tokens; fixed). Wave-parallel with N workers. + LAT_PER_KTOK = 12.0 # seconds per 1k output tokens (mock, order-of-magnitude) + draft_lat = [max(3.0, CK.est_out(c) / 1000 * LAT_PER_KTOK) for c in draft_chunks] + edit_lat = [max(3.0, CK.est_out(u) / 1000 * LAT_PER_KTOK) for u in edit_units] + def wave_wall(lats, workers): + # greedy longest-processing-time bin packing onto `workers` lanes + lanes = [0.0] * workers + for t in sorted(lats, reverse=True): + i = lanes.index(min(lanes)); lanes[i] += t + return round(max(lanes), 1) + result["wall_clock_seconds"] = { + "sequential_total": round(sum(draft_lat) + sum(edit_lat), 1), + "wave_W1_4workers": wave_wall(draft_lat, 4), "wave_W2_4workers": wave_wall(edit_lat, 4), + "wave_W1_8workers": wave_wall(draft_lat, 8), "wave_W2_8workers": wave_wall(edit_lat, 8), + "wave_total_8workers": wave_wall(draft_lat, 8) + wave_wall(edit_lat, 8), + "speedup_8w": round((sum(draft_lat) + sum(edit_lat)) / (wave_wall(draft_lat, 8) + wave_wall(edit_lat, 8)), 2), + } + + # (3) COGS per editor-arm x cache x peak + result["cogs_per_arm_usd"] = {} + for editor in ("glm-5", "mistral-large-latest", "deepseek-v4-pro"): + arm = {} + for cache in (True, False): + for peak in (False, True): + d, e, tot = cogs(draft_chunks, edit_units, editor, cache=cache, peak=peak) + arm[f"cache={cache}_peak={peak}"] = {"draft": d, "edit": e, "total": tot} + result["cogs_per_arm_usd"][editor] = arm + # headline: warm cache, valley (recommended operating point) + result["cogs_headline_25ch_slice"] = { + editor: result["cogs_per_arm_usd"][editor]["cache=True_peak=False"]["total"] + for editor in ("glm-5", "mistral-large-latest", "deepseek-v4-pro")} + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + json.dump(result, open(OUT, "w"), ensure_ascii=False, indent=2) + print(json.dumps(result, ensure_ascii=False, indent=2)) + mi = result["money_invariant_test"] + print(f"\nVERDICT: money-invariant {'ALL PASS' if mi['all_pass'] else 'FAIL'}; " + f"wall-clock speedup 8w = {result['wall_clock_seconds']['speedup_8w']}x; " + f"COGS/25ch (warm,valley): " + ", ".join(f"{k.split('-')[0]}=${v}" for k, v in result['cogs_headline_25ch_slice'].items())) + +if __name__ == "__main__": + main() diff --git a/eval/design11/ws2_chunk_sim.py b/eval/design11/ws2_chunk_sim.py new file mode 100644 index 0000000..85fd684 --- /dev/null +++ b/eval/design11/ws2_chunk_sim.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""WS2 (г) verification — chunking + edit-unit simulation on the 25-chapter book. + +Reconstructs per-chapter source from rerun/records.json, then simulates: + (1) DRAFT chunking under the OUTPUT-token budget (fertility est_out), for target_out + in {1500, 1797, 2200}, vs the CURRENT input-est greedy budget (targetChunkTokens=1500); + (2) EDIT-UNIT = chapter (with paragraph-split when a chapter exceeds a ceiling). +Reports: est_out histograms, share in the 2k-8k-out degradation zone (research/19 §A.1.1), +floor-invariant (chunks under MinChunkChars=500 src chars), chapters>ceiling, chunk/unit counts, +and a COGS forecast via fertility (flash draft + glm-5 edit). + +$0: deterministic, no LLM, no network. Reuses eval/exp15/chunker.py greedy+sentence helpers. +Writes /home/ubuntu/books/gu-zhenren/design11/ws2_chunk_sim.json (OUT of git). +""" +import json, os, sys +from collections import defaultdict, Counter + +sys.path.insert(0, "/home/ubuntu/projects/textmachine/eval/exp15") +import chunker as CK # greedy_chunks, est_out, _est_tokens_from, _class_counts, _pack_sentences, split_paragraphs, normalize_source + +RECORDS = "/home/ubuntu/books/gu-zhenren/rerun/records.json" +OUT = "/home/ubuntu/books/gu-zhenren/design11/ws2_chunk_sim.json" +F_CJK, F_OTHER = 1.1978, 0.3852 +MIN_CHUNK_CHARS = 500 # Gates.Coverage.MinChunkChars (pipeline.go:159) +DEG_LO, DEG_HI = 2000, 8000 # emission degradation zone, output tokens (research/19 §A.1.1) +# prices USD/1M (freeze_facts / vendor-verified): draft flash out, edit glm-5 out (thinking-off) +PRICE = {"flash_in": 0.14, "flash_cache": 0.0028, "flash_out": 0.28, + "glm5_in": 1.00, "glm5_cache": 0.20, "glm5_out": 3.20} + +def est_out(text): + return CK.est_out(text) # F_CJK*cjk + F_OTHER*other on source chars + +def build_chapters(): + recs = json.load(open(RECORDS)) + by_ch = defaultdict(list) + for r in recs: + by_ch[r["chapter"]].append((r["chunk_idx"], r.get("source", "") or "")) + chapters = {} + for ch, items in by_ch.items(): + items.sort() + chapters[ch] = "\n".join(s for _, s in items) + return dict(sorted(chapters.items())) + +def greedy_out(text, target_out): + """Greedy paragraph pack to an OUTPUT-token budget (fertility est_out), mirroring + appendChapterChunks but with the budget in ru-output tokens. Oversize paragraph -> + sentence pack; oversize sentence -> own chunk (never split a sentence).""" + chapter = CK.normalize_source(text) + paras = CK.split_paragraphs(chapter) + chunks, buf = [], [] + def buf_out(): + return est_out("\n\n".join(buf)) if buf else 0.0 + def flush(): + nonlocal buf + if buf: + t = "\n\n".join(buf).strip() + if t: + chunks.append(t) + buf = [] + for p in paras: + if est_out(p) > target_out: + flush() + # sentence pack this oversize paragraph to target_out + segs = CK.split_source_sentences(p) + sbuf = [] + def sflush(): + nonlocal sbuf + if sbuf: + chunks.append("".join(sbuf)) + sbuf = [] + for s in segs: + if sbuf and est_out("".join(sbuf) + s) > target_out: + sflush() + sbuf.append(s) + sflush() + continue + if buf and est_out("\n\n".join(buf) + "\n\n" + p) > target_out: + flush() + buf.append(p) + flush() + return chunks + +def nonspace_chars(s): + return sum(1 for c in s if not c.isspace()) + +def hist(values, edges): + c = Counter() + for v in values: + placed = False + for lo, hi in edges: + if lo <= v < hi: + c[f"{lo}-{hi}"] += 1; placed = True; break + if not placed: + c[f">={edges[-1][1]}"] += 1 + return dict(c) + +def summarize_chunks(chunks): + outs = [est_out(c) for c in chunks] + src_chars = [nonspace_chars(c) for c in chunks] + return { + "n_chunks": len(chunks), + "est_out_mean": round(sum(outs)/len(outs), 1) if outs else 0, + "est_out_min": round(min(outs), 1) if outs else 0, + "est_out_max": round(max(outs), 1) if outs else 0, + "under_min_chunk_chars(500)": sum(1 for s in src_chars if s < MIN_CHUNK_CHARS), + "in_degradation_zone_2k_8k": sum(1 for o in outs if DEG_LO <= o <= DEG_HI), + "above_deg_hi(8k)": sum(1 for o in outs if o > DEG_HI), + "hist_est_out": hist(outs, [(0,1000),(1000,2000),(2000,3200),(3200,5000),(5000,8000)]), + "total_est_out": round(sum(outs)), + } + +def main(): + chapters = build_chapters() + result = {"n_chapters": len(chapters), "MIN_CHUNK_CHARS": MIN_CHUNK_CHARS, + "degradation_zone_out_tokens": [DEG_LO, DEG_HI]} + + # --- (a) CURRENT chunking: greedy on input-est targetChunkTokens=1500 (chunker.go behavior) --- + cur_chunks = [] + for ch, txt in chapters.items(): + cur_chunks += CK.greedy_chunks(txt) # input-est 1500 budget (backend const) + result["current_greedy_input_est_1500"] = summarize_chunks(cur_chunks) + + # --- (b) DRAFT chunking under OUTPUT-token budget (fertility), swept target_out --- + result["draft_output_budget"] = {} + for tgt in (1500, 1797, 2200): + dchunks = [] + for ch, txt in chapters.items(): + dchunks += greedy_out(txt, tgt) + result["draft_output_budget"][f"target_out_{tgt}"] = summarize_chunks(dchunks) + + # --- (c) EDIT-UNIT = chapter; paragraph-split when chapter est_out > ceiling --- + for ceiling in (8000, 3200): # 8000 = gated large-chapter arm; 3200 = ratified default (review-2 F1) + units = [] + chapters_over = 0 + split_units = 0 + for ch, txt in chapters.items(): + co = est_out(txt) + if co <= ceiling: + units.append(txt) + else: + chapters_over += 1 + # split at paragraph boundaries greedily to <= ceiling (never split a paragraph) + sub = greedy_out(txt, ceiling) + split_units += len(sub) + units += sub + s = summarize_chunks(units) + s["chapters_over_ceiling"] = chapters_over + s["units_from_split_chapters"] = split_units + s["chapter_est_out_raw"] = {str(ch): round(est_out(txt)) for ch, txt in chapters.items()} + result[f"edit_unit_chapter_ceiling_{ceiling}"] = s + + # --- (d) COGS forecast per structure (input via records source, output via fertility) --- + # draft: input = source est-tokens; output = est_out. edit: input = source+draft (~2x src) est; output ~= 0.85*draft_out (edit shrinks slightly) + total_src_est_in = sum(CK._est_tokens_from(*CK._class_counts(txt)) for txt in chapters.values()) + total_draft_out = sum(est_out(txt) for txt in chapters.values()) + # naive per-book COGS on the whole book (no cache) as an upper bound; cache handled in WS1/WS6 + draft_in_cost = total_src_est_in * PRICE["flash_in"] / 1e6 + draft_out_cost = total_draft_out * PRICE["flash_out"] / 1e6 + edit_in_est = (total_src_est_in + total_draft_out) # editor sees src + draft + edit_in_cost = edit_in_est * PRICE["glm5_in"] / 1e6 + edit_out_cost = (total_draft_out * 0.9) * PRICE["glm5_out"] / 1e6 # edit ~= draft length + result["cogs_forecast_25ch_no_cache_upper_bound"] = { + "total_src_est_in_tokens": round(total_src_est_in), + "total_draft_out_tokens": round(total_draft_out), + "draft_cost_usd": round(draft_in_cost + draft_out_cost, 4), + "edit_cost_usd": round(edit_in_cost + edit_out_cost, 4), + "total_usd": round(draft_in_cost + draft_out_cost + edit_in_cost + edit_out_cost, 4), + "note": "upper bound, no prefix cache; WS1/WS6 apply warm-then-fan cache (-80% cached input)", + } + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + json.dump(result, open(OUT, "w"), ensure_ascii=False, indent=2) + print(json.dumps(result, ensure_ascii=False, indent=2)) + +if __name__ == "__main__": + main() diff --git a/eval/design11/ws2_fertility_verify.py b/eval/design11/ws2_fertility_verify.py new file mode 100644 index 0000000..ec37d29 --- /dev/null +++ b/eval/design11/ws2_fertility_verify.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""WS2 (г) verification — INDEPENDENT re-derivation of the output-token fertility +coefficients (L2-budget-wrong-unit fix), NOT trusting fertility_calib.json. + +Reproduces est_out = f_cjk*cjk_src + f_other*other_src (OLS, no intercept) over the +25-chapter guzhenren rerun, and cross-checks the §1.6 finding: fertility_calib's +ord-range CJK classifier differs from the backend EstimateTokens class set +(unicode.Han|Hiragana|Katakana|Hangul). We fit under BOTH classifiers and report the +coefficient delta so the plan can decide (align vs document-invariance). + +$0: deterministic, no LLM, no network. Reuses eval/tokenizers/glm-4.6 + rerun/records.json. +Writes result to /home/ubuntu/books/gu-zhenren/design11/ws2_fertility_verify.json (OUT of git). +""" +import json, os, sys +import numpy as np +from tokenizers import Tokenizer + +ROOT = "/home/ubuntu/projects/textmachine" +RECORDS = "/home/ubuntu/books/gu-zhenren/rerun/records.json" +TOK = f"{ROOT}/eval/tokenizers/glm-4.6/tokenizer.json" +STORED = "/home/ubuntu/books/gu-zhenren/exp15/fertility_calib.json" +OUT = "/home/ubuntu/books/gu-zhenren/design11/ws2_fertility_verify.json" + +# ---- classifier A: fertility_calib.py is_cjk (ord ranges, NO Hangul) ---- +def is_cjk_ord(c): + o = ord(c) + return (0x4E00 <= o <= 0x9FFF or 0x3040 <= o <= 0x30FF or 0x3400 <= o <= 0x4DBF + or 0xF900 <= o <= 0xFAFF or 0x20000 <= o <= 0x2A6DF) + +# ---- classifier B: backend EstimateTokens (unicode property Han|Hira|Kata|Hangul) ---- +import unicodedata +def is_cjk_prop(c): + # Approximate Go unicode.Han|Hiragana|Katakana|Hangul via Unicode script. + # Python has no stdlib script table; use unicodedata name prefixes as a proxy that + # matches the Go ranges for the scripts in question. + try: + name = unicodedata.name(c) + except ValueError: + return False + o = ord(c) + # Han (CJK ideographs incl ext), Hiragana, Katakana, Hangul syllables/jamo + if (0x3400 <= o <= 0x9FFF or 0xF900 <= o <= 0xFAFF or 0x20000 <= o <= 0x2FA1F): + return "CJK" in name or "IDEOGRAPH" in name + if 0x3040 <= o <= 0x309F: return True # Hiragana + if 0x30A0 <= o <= 0x30FF: return True # Katakana (incl. 0x30FB middle dot? Go: Katakana table) + if 0xAC00 <= o <= 0xD7A3 or 0x1100 <= o <= 0x11FF or 0x3130 <= o <= 0x318F: return True # Hangul + return False + +def classes(text, is_cjk): + cjk = other = 0 + for c in text: + if c.isspace(): + continue + if is_cjk(c): + cjk += 1 + else: + other += 1 + return cjk, other + +def fit(rows): + A = np.array([[c, o] for (c, o, _) in rows], dtype=float) + y = np.array([t for (_, _, t) in rows], dtype=float) + coef, *_ = np.linalg.lstsq(A, y, rcond=None) + pred = A @ coef + ss_res = float(np.sum((y - pred) ** 2)) + ss_tot = float(np.sum((y - y.mean()) ** 2)) + r2 = 1 - ss_res / ss_tot + mape = float(np.mean(np.abs(y - pred) / y) * 100) + return float(coef[0]), float(coef[1]), r2, mape + +def main(): + tok = Tokenizer.from_file(TOK) + recs = json.load(open(RECORDS)) + rows_A, rows_B = [], [] + used = skipped_empty = skipped_tiny = 0 + for r in recs: + src = r.get("source", "") or "" + fin = r.get("final", "") or "" + if not src.strip() or not fin.strip(): + skipped_empty += 1 + continue + cA, oA = classes(src, is_cjk_ord) + if cA + oA < 200: + skipped_tiny += 1 + continue + out_tok = len(tok.encode(fin).ids) + cB, oB = classes(src, is_cjk_prop) + rows_A.append((cA, oA, out_tok)) + rows_B.append((cB, oB, out_tok)) + used += 1 + + fa = fit(rows_A) # classifier A (ord ranges, as fertility_calib) + fb = fit(rows_B) # classifier B (backend property set) + stored = json.load(open(STORED)) + + result = { + "n_records_used": used, "skipped_empty": skipped_empty, "skipped_tiny": skipped_tiny, + "total_records": len(recs), + "classifier_A_ord_ranges (fertility_calib)": { + "f_cjk": round(fa[0], 4), "f_other": round(fa[1], 4), + "r2": round(fa[2], 4), "mape_pct": round(fa[3], 2)}, + "classifier_B_backend_property (Han|Hira|Kata|Hangul)": { + "f_cjk": round(fb[0], 4), "f_other": round(fb[1], 4), + "r2": round(fb[2], 4), "mape_pct": round(fb[3], 2)}, + "stored_artifact": {"f_cjk": stored["f_cjk"], "f_other": stored["f_other"], + "r2": stored["r2"], "mape_pct": stored["mape_pct"], + "n_records": stored["n_records"]}, + "coef_delta_A_vs_stored": {"f_cjk": round(abs(fa[0] - stored["f_cjk"]), 6), + "f_other": round(abs(fa[1] - stored["f_other"]), 6)}, + "coef_delta_B_vs_A (classifier mismatch impact)": { + "f_cjk": round(fb[0] - fa[0], 4), "f_other": round(fb[1] - fa[1], 4), + "pct_est_out_shift_on_median_zh_chunk": None}, + } + # quantify classifier mismatch: predicted est_out on the median src chunk under A vs B + med = rows_A[len(rows_A)//2] + estA = fa[0]*med[0] + fa[1]*med[1] + # use classifier B counts for the same chunk under fit B + medB = rows_B[len(rows_B)//2] + estB = fb[0]*medB[0] + fb[1]*medB[1] + result["coef_delta_B_vs_A (classifier mismatch impact)"]["pct_est_out_shift_on_median_zh_chunk"] = round((estB-estA)/estA*100, 3) + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + json.dump(result, open(OUT, "w"), ensure_ascii=False, indent=2) + print(json.dumps(result, ensure_ascii=False, indent=2)) + # PASS assertion: independent re-derivation matches stored to <1e-3 + dA = result["coef_delta_A_vs_stored"] + ok = dA["f_cjk"] < 1e-3 and dA["f_other"] < 1e-3 + print(f"\nVERDICT: independent re-derivation {'REPRODUCES' if ok else 'DIVERGES FROM'} stored coefficients " + f"(Δf_cjk={dA['f_cjk']}, Δf_other={dA['f_other']}).") + +if __name__ == "__main__": + main() diff --git a/eval/design11/ws3_miner_verify.py b/eval/design11/ws3_miner_verify.py new file mode 100644 index 0000000..107b1e7 --- /dev/null +++ b/eval/design11/ws3_miner_verify.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""WS3 (г) verification — re-run the frozen miner-v1 (A3 = V-C) on the pinned exp16 +inputs and confirm it reproduces the golden fixtures that pin the FUTURE Go port. + +Does NOT overwrite the golden (writes only to design11). Asserts: + - A3 catastrophe ranks {方源:0, 蛊:1, 蛊师:2, 古月:21} on the full slice (PASS <50); + - A3 recall@proposed = 0.965 (full) / 0.932 (test-half); + - A3 candidate count = 13618 (full). +Compares value-by-value against the stored golden JSON (the byte-parity harness in +mem_select-reverse: Python = reference, Go port must reproduce). + +$0: deterministic, no LLM, no network. Reuses eval/exp16 miner-v1 (frozen SHAs §1.10). +Writes /home/ubuntu/books/gu-zhenren/design11/ws3_miner_verify.json (OUT of git). +""" +import json, os, sys + +EVAL = "/home/ubuntu/projects/textmachine/eval" +# exp15 first, then exp16 at position 0 so exp16/arms.py wins over exp15/arms.py +# (name collision the exp16_common self-review flagged; it uses sys.path.append for exp15/mem_select). +sys.path.insert(0, f"{EVAL}/exp15") +sys.path.insert(0, f"{EVAL}/exp16") + +import exp16_common as X +import arms as A + +GOLD_DIR = "/home/ubuntu/books/gu-zhenren/exp16" +OUT = "/home/ubuntu/books/gu-zhenren/design11/ws3_miner_verify.json" +CAT_EXPECT = {"方源": 0, "蛊": 1, "蛊师": 2, "古月": 21} + +def rank_of(ranked, srcnorm): + for i, c in enumerate(ranked): + if c.src == srcnorm: + return i + return None + +def main(): + chunks = X.load_chunks() + gt = X.load_gt() + contrast = X.Contrast() + cfg = A.FROZEN + + arms = A.Arms(chunks, contrast, cfg, use_spread=True) + a3_full = arms.arm_A3(cfg["lam"]) + + # catastrophe ranks on full A3 + cat_ranks = {} + for term in X.CATASTROPHE: + cat_ranks[term] = rank_of(a3_full, X.norm(term)) + cat_pass = all(r is not None and r < 50 for r in cat_ranks.values()) + + # recall@proposed full + rec_full, _ = A.recall_by(a3_full, gt, chunks, top_k=None, include_annotation=True) + overall_full = rec_full["overall"][0] + + # test half (ch16-25) — GT filtered to entities occurring in test chapters (run_arms phase_test) + test_chunks = [c for c in chunks if c.chapter in X.TEST_CH] + gt_t = A.gt_in_chapters(gt, chunks, X.TEST_CH) + arms_t = A.Arms(test_chunks, contrast, cfg, use_spread=True) + a3_test = arms_t.arm_A3(cfg["lam"]) + rec_test, _ = A.recall_by(a3_test, gt_t, test_chunks, top_k=None, include_annotation=True) + overall_test = rec_test["overall"][0] + + # load stored golden for value-by-value compare + gold_full = json.load(open(f"{GOLD_DIR}/full_slice_result.json")) + gold_test = json.load(open(f"{GOLD_DIR}/test_half_result.json")) + gold_a3_full = gold_full.get("A3", {}) + gold_a3_test = gold_test.get("A3", {}) + n_cands_full = len(a3_full) + + result = { + "catastrophe_ranks_A3_full": cat_ranks, + "catastrophe_expected": CAT_EXPECT, + "catastrophe_match": cat_ranks == CAT_EXPECT, + "catastrophe_screen_pass": cat_pass, + "recall_at_proposed_A3_full": round(overall_full, 4) if overall_full is not None else None, + "recall_at_proposed_A3_test": round(overall_test, 4) if overall_test is not None else None, + "n_candidates_A3_full": n_cands_full, + "golden_full_overall": gold_a3_full.get("recall_proposed", {}).get("overall") if isinstance(gold_a3_full.get("recall_proposed"), dict) else gold_a3_full.get("recall_proposed"), + "golden_test_overall": gold_a3_test.get("recall_proposed", {}).get("overall") if isinstance(gold_a3_test.get("recall_proposed"), dict) else gold_a3_test.get("recall_proposed"), + "top20_A3_full": [{"src": c.src, "score": round(c.score, 2), "freq": c.freq, + "types": c.types, "from_pattern": c.from_pattern} for c in a3_full[:20]], + } + os.makedirs(os.path.dirname(OUT), exist_ok=True) + json.dump(result, open(OUT, "w"), ensure_ascii=False, indent=2) + print(json.dumps({k: v for k, v in result.items() if k != "top20_A3_full"}, ensure_ascii=False, indent=2)) + print("\ntop-5 A3 full:", [(c.src, round(c.score,1), c.freq) for c in a3_full[:5]]) + print(f"\nVERDICT: catastrophe {'MATCH' if result['catastrophe_match'] else 'MISMATCH'} {cat_ranks}; " + f"n_cands={n_cands_full} (golden 13618); recall_full={result['recall_at_proposed_A3_full']} (golden 0.965).") + +if __name__ == "__main__": + main() diff --git a/eval/design11/ws3_seeddelta.py b/eval/design11/ws3_seeddelta.py new file mode 100644 index 0000000..de56f79 --- /dev/null +++ b/eval/design11/ws3_seeddelta.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""WS3 (г) verification, part 2 — emit a seed-delta (§C2-7) from the persisted exp16 +tables (A3 candidates + banknote dst), which exp16 itself never emitted (only sidecars), +then DRY-LINT it against a faithful Python replication of the memseed.loadGlossarySeed +fail-louds (the (д) after-build spec runs the real Go loadGlossarySeed for byte parity). + +Emission rules (research/20 §C2-7, memseed.go:30-57 seedTerm schema, miner NEVER writes approved): + - non-seed A3 candidate WITH a banknote dst -> status: draft, dst= + - non-seed A3 candidate WITHOUT a dst -> status: auto (no dst, inert in automaton) + - since_ch = first chapter of occurrence (auto, by source); until_ch left 0 (owner only) + +$0: deterministic, no LLM, no network. Reuses eval/exp16 miner + banknote artifacts. +Writes seed-delta YAML + lint report to /home/ubuntu/books/gu-zhenren/design11/ (OUT of git). +""" +import json, os, sys, glob, re, collections +import yaml + +EVAL = "/home/ubuntu/projects/textmachine/eval" +sys.path.insert(0, f"{EVAL}/exp15") +sys.path.insert(0, f"{EVAL}/exp16") +import exp16_common as X +import arms as A +import mem_select as MS # normalize_source_key, significant_len (byte-verified port of memnorm.go) + +A3 = "/home/ubuntu/books/gu-zhenren/exp16/A3_candidates_full.json" +BANK = "/home/ubuntu/books/gu-zhenren/exp16/coldstart/banknote/*.banknote.json" +OUT_YAML = "/home/ubuntu/books/gu-zhenren/design11/ws3_seed_delta.yaml" +OUT_LINT = "/home/ubuntu/books/gu-zhenren/design11/ws3_seeddelta_lint.json" + +MIN_KEY_HAN, MIN_KEY_PHON = 2, 3 +def min_key_len(nk): + return MIN_KEY_HAN if any('㐀' <= c <= '鿿' or '豈' <= c <= '﫿' for c in nk) else MIN_KEY_PHON + +# ---------- faithful replication of memseed.loadGlossarySeed fail-louds ---------- +def lint_seed(terms): + problems = [] + seen = {} + for i, t in enumerate(terms): + src = (t.get("src") or "").strip() + sense = (t.get("sense") or "").strip() + dst = (t.get("dst") or "").strip() + if not src: + problems.append(f"term {i}: src is required"); continue + status = t.get("status") or "approved" + if status not in ("auto", "draft", "approved"): + problems.append(f"term {src!r}: status must be auto|draft|approved, got {status!r}"); continue + if status in ("approved", "draft") and not dst: + problems.append(f"term {src!r}: a {status} term must have a non-empty dst (silently inert)"); continue + key = (src, sense, t.get("since_ch", 0), t.get("until_ch", 0)) + if key in seen: + problems.append(f"term {src!r}: duplicate (src,sense,window)"); continue + seen[key] = i + # spoiler-window overlap same src different dst (D16.1) + shared-key collision (approved only) + def win_overlap(s1, u1, s2, u2): + s1 = s1 or 1; s2 = s2 or 1; u1 = u1 or (1 << 62); u2 = u2 or (1 << 62) + return max(s1, s2) <= min(u1, u2) + for i in range(len(terms)): + for j in range(i): + a, b = terms[i], terms[j] + if a.get("src") != b.get("src"): + continue + if (a.get("dst") or "") == (b.get("dst") or ""): + continue + if win_overlap(a.get("since_ch", 0), a.get("until_ch", 0), b.get("since_ch", 0), b.get("until_ch", 0)): + problems.append(f"term {a['src']!r}: overlapping spoiler windows with different dst") + return problems + +def is_fragment(src, gt_norm, particles="的了在是和就也都不心面前后上下今少多大小与之其"): + if len(src) >= 2 and MS.normalize_source_key(src[:-1]) in gt_norm and src[-1] in particles: + return True + return False + +def main(): + chunks = X.load_chunks() + gt = X.load_gt() + gt_norm = set() + for e in gt: + for s in e.norm_surfaces: + gt_norm.add(s) + + a3 = json.load(open(A3)) + # banknote dst: majority per src; strip trailing [type] annotations the model added + bn = collections.defaultdict(collections.Counter) + for f in glob.glob(BANK): + d = json.load(open(f)) + # chapter from filename e.g. 4.0.banknote.json -> chapter 4 + ch = int(os.path.basename(f).split(".")[0]) + for e in d.get("entries", []): + dst = re.sub(r"\[[^\]]*\]", "", e["dst"]).strip() + bn[MS.normalize_source_key(e["src"])][(dst, e.get("type", "term"))] += 1 + bn_first_ch = {} + for f in glob.glob(BANK): + ch = int(os.path.basename(f).split(".")[0]) + d = json.load(open(f)) + for e in d.get("entries", []): + k = MS.normalize_source_key(e["src"]) + bn_first_ch[k] = min(bn_first_ch.get(k, 1 << 30), ch) + + # candidate chapters helper (first chapter of occurrence) + def first_chapter(src_norm): + for c in chunks: + if src_norm in c.nsource: + return c.chapter + return 0 + + delta = [] + seen_src = set() + n_draft = n_auto = 0 + # (1) non-seed candidates WITH a banknote dst -> draft + for cand in a3: + srcn = MS.normalize_source_key(cand["src"]) + if srcn in gt_norm or srcn in seen_src: + continue + if is_fragment(cand["src"], gt_norm): + continue + typ = (cand["types"] or ["term"])[0] + if srcn in bn: + (dst, bt) = bn[srcn].most_common(1)[0][0], bn[srcn].most_common(1)[0] + dstv = bn[srcn].most_common(1)[0][0][0] + if not dstv: + continue + delta.append({"src": cand["src"], "dst": dstv, "type": typ, "status": "draft", + "since_ch": bn_first_ch.get(srcn) or first_chapter(srcn), + "note": "mined draft (banknote dst); ⟨проверить⟩ — canon proposal, owner signs"}) + seen_src.add(srcn); n_draft += 1 + # (2) top non-seed WHICH-only candidates WITHOUT dst -> auto (inert), a bounded sample. + # Apply the miner's operating point (top_k=90 rank window) + single-key ban + fragment filter + # so the owner review-sheet is clean (raw 13618-dump would carry particle/fragment noise). + a3_top = a3[: A.FROZEN["top_k"] * 3] # generous rank window; only pattern-typed or freq-anchored survive filters + for cand in a3_top: + if n_auto >= 40: + break + srcn = MS.normalize_source_key(cand["src"]) + if srcn in gt_norm or srcn in seen_src: + continue + if is_fragment(cand["src"], gt_norm): + continue + if MS.significant_len(srcn) < min_key_len(srcn): + continue # single-key ban (A3): a too-short key never fires / is not an entity + if cand["freq"] < X.FREQ_FLOOR and not cand["from_pattern"]: + continue + typ = (cand["types"] or ["term"])[0] + if typ not in ("name", "place", "title", "term", "nickname"): + typ = "term" + delta.append({"src": cand["src"], "dst": "", "type": typ, "status": "auto", + "since_ch": first_chapter(srcn), + "note": "mined WHICH-only auto-candidate (no dst; inert until owner assigns)"}) + seen_src.add(srcn); n_auto += 1 + + seed_doc = {"terms": delta} + os.makedirs(os.path.dirname(OUT_YAML), exist_ok=True) + with open(OUT_YAML, "w") as fh: + yaml.safe_dump(seed_doc, fh, allow_unicode=True, sort_keys=False) + + # round-trip: re-parse the emitted YAML and lint it (as loadGlossarySeed would) + reparsed = yaml.safe_load(open(OUT_YAML))["terms"] + problems = lint_seed(reparsed) + + report = { + "n_delta_terms": len(delta), "n_draft_with_banknote_dst": n_draft, "n_auto_no_dst": n_auto, + "lint_problems": problems, "lint_pass": len(problems) == 0, + "sample_draft": [t for t in delta if t["status"] == "draft"][:8], + "sample_auto": [t for t in delta if t["status"] == "auto"][:8], + } + json.dump(report, open(OUT_LINT, "w"), ensure_ascii=False, indent=2) + print(json.dumps({k: report[k] for k in ("n_delta_terms", "n_draft_with_banknote_dst", "n_auto_no_dst", "lint_pass", "lint_problems")}, ensure_ascii=False, indent=2)) + print("\nsample draft (mined dst):", [(t["src"], t["dst"], t["type"]) for t in report["sample_draft"]]) + print("sample auto (which-only):", [(t["src"], t["type"]) for t in report["sample_auto"]]) + print(f"\nVERDICT: seed-delta of {len(delta)} terms ({n_draft} draft+dst / {n_auto} auto) " + f"{'PASSES' if report['lint_pass'] else 'FAILS'} memseed fail-loud dry-lint.") + +if __name__ == "__main__": + main() diff --git a/eval/design11/ws4_banknote_verify.py b/eval/design11/ws4_banknote_verify.py new file mode 100644 index 0000000..4baae8b --- /dev/null +++ b/eval/design11/ws4_banknote_verify.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""WS4 (г) verification — banknote-v1 channel parser. + + (1) Re-parse the 14 persisted raw model outputs with the frozen banknote-v1 parser and + reproduce: 95 total lines, distinct_src 65, parse_fail 0.0%, truncated 0. + (2) Synthetic truncation: a block whose LAST line is cut mid-field parses tolerantly + (banknote_truncated) under truncated_generation=True and as bad (parse_fail) under False. + (3) fresh<->resume equivalence: the tm-banknote-v1 DERIVED-checkpoint id is a content-addressed + hash of (reqHash, stripped) with the exact commitSanitizedExport formula (NULL-byte separators), + so a fresh run and a resume re-derive the IDENTICAL id => the OK-path final_hash re-point is + resume-stable by construction. + (4) Parser invariants: SEP not a note-word; src must contain Han; tolerant field split (tab|>=2sp|pipe). + +$0: deterministic, no LLM, no network. Reuses eval/exp16/banknote.py + persisted raw outputs. +Writes /home/ubuntu/books/gu-zhenren/design11/ws4_banknote_verify.json (OUT of git). +""" +import json, os, sys, glob, hashlib + +sys.path.insert(0, "/home/ubuntu/projects/textmachine/eval/exp16") +import banknote as BN + +RAW = "/home/ubuntu/books/gu-zhenren/exp16/coldstart/banknote/*.raw.txt" +OUT = "/home/ubuntu/books/gu-zhenren/design11/ws4_banknote_verify.json" + +def derived_hash(req_hash, stripped, channel="tm-banknote-v1"): + """Exact commitSanitizedExport precedent (stagerun.go:230-232): NULL-byte separators + version + INSIDE the hash; id prefix = channel + ':'. This is the WS4 tm-banknote-v1 derived-checkpoint id.""" + payload = (channel + "\x00" + req_hash + "\x00" + stripped).encode("utf-8") + return channel + ":" + hashlib.sha256(payload).hexdigest() + +def main(): + result = {} + + # ---- (1) re-parse the 14 persisted raw outputs ---- + total_lines = 0 + parse_fails = 0 + truncated = 0 + distinct_src = set() + per_file = {} + for f in sorted(glob.glob(RAW)): + raw = open(f).read() + clean, block = BN.split_banknote(raw) + entries, flags = BN.parse_banknote(block, truncated_generation=False) # prod: stop-only gate => False + total_lines += flags["n_banknote_lines"] + parse_fails += 1 if flags["banknote_parse_fail"] else 0 + truncated += 1 if flags["banknote_truncated"] else 0 + for e in entries: + distinct_src.add(e["src"]) + # invariant: clean must NOT contain the SEP + assert BN.SEP not in clean, f"{f}: SEP leaked into clean text" + per_file[os.path.basename(f)] = {"lines": flags["n_banknote_lines"], + "parse_fail": flags["banknote_parse_fail"]} + result["reparse_14_files"] = { + "total_banknote_lines": total_lines, "distinct_src": len(distinct_src), + "files_with_parse_fail": parse_fails, "files_truncated": truncated, + "expected": {"total_lines": 95, "distinct_src": 65, "parse_fail": 0, "truncated": 0}, + "match": (total_lines == 95 and len(distinct_src) == 65 and parse_fails == 0 and truncated == 0), + } + + # ---- (2) synthetic truncation ---- + good_block = "蛊\tгу\tterm\n方源\tФан Юань\tname" + trunc_block = good_block + "\n希望蛊" # last line cut before the dst/type field + e_t, f_t = BN.parse_banknote(trunc_block, truncated_generation=True) + e_f, f_f = BN.parse_banknote(trunc_block, truncated_generation=False) + result["synthetic_truncation"] = { + "truncated_gen_True": {"n_lines": f_t["n_banknote_lines"], "parse_fail": f_t["banknote_parse_fail"], + "truncated": f_t["banknote_truncated"]}, + "truncated_gen_False": {"n_lines": f_f["n_banknote_lines"], "parse_fail": f_f["banknote_parse_fail"], + "truncated": f_f["banknote_truncated"]}, + "verdict": ("tolerant under True (truncated flag, 2 good lines kept), bad under False (parse_fail)" + if (f_t["banknote_truncated"] and not f_t["banknote_parse_fail"] and f_t["n_banknote_lines"] == 2 + and f_f["banknote_parse_fail"]) else "UNEXPECTED"), + } + + # ---- (3) fresh<->resume derived-hash equivalence ---- + req_hash = "a1b2c3deadbeef" * 4 # a sample attempt request_hash + stripped = "Чистовик перевода без блока сноски." + h1 = derived_hash(req_hash, stripped) # fresh run + h2 = derived_hash(req_hash, stripped) # resume re-derive + h3 = derived_hash(req_hash, stripped + "x") # different stripped -> different id + result["fresh_resume_hash"] = { + "id_fresh": h1[:40] + "...", "id_resume": h1 == h2, "stable": h1 == h2, + "collision_free_on_content_change": h1 != h3, + "formula": "sha256('tm-banknote-v1\\x00'+reqHash+'\\x00'+stripped), id-prefix 'tm-banknote-v1:'", + "verdict": "resume re-derives IDENTICAL id (content-addressed) => OK-path final_hash re-point resume-stable" + if (h1 == h2 and h1 != h3) else "UNEXPECTED", + } + + # ---- (4) parser invariants ---- + inv = {} + # SEP is not a reserved note-word + inv["SEP_not_note_word"] = all(w not in BN.SEP for w in ("Примечание", "Сноска", "Комментарий", "Note")) + # non-Han src -> bad + _, f_nh = BN.parse_banknote("hello\tworld\tterm") + inv["non_han_src_is_bad"] = f_nh["banknote_parse_fail"] + # SEP-less output -> whole thing is clean, empty block + clean_ns, block_ns = BN.split_banknote("Просто перевод без блока.") + inv["no_sep_all_clean"] = (block_ns == "" and clean_ns == "Просто перевод без блока.") + # tolerant field split: >=2 spaces and pipe both parse + e_sp, f_sp = BN.parse_banknote("蛊 гу term") + e_pipe, f_pipe = BN.parse_banknote("蛊 | гу | term") + inv["tolerant_split_spaces"] = (not f_sp["banknote_parse_fail"] and f_sp["n_banknote_lines"] == 1) + inv["tolerant_split_pipe"] = (not f_pipe["banknote_parse_fail"] and f_pipe["n_banknote_lines"] == 1) + result["parser_invariants"] = inv + result["parser_invariants_all_pass"] = all(inv.values()) + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + json.dump(result, open(OUT, "w"), ensure_ascii=False, indent=2) + print(json.dumps(result, ensure_ascii=False, indent=2)) + r1 = result["reparse_14_files"] + print(f"\nVERDICT: reparse {r1['total_banknote_lines']} lines / {r1['distinct_src']} distinct_src, " + f"parse_fail={r1['files_with_parse_fail']} truncated={r1['files_truncated']} " + f"({'MATCH' if r1['match'] else 'MISMATCH'} golden 95/65/0/0); " + f"truncation {result['synthetic_truncation']['verdict'][:30]}...; " + f"hash {'STABLE' if result['fresh_resume_hash']['stable'] else 'UNSTABLE'}; " + f"invariants {'ALL PASS' if result['parser_invariants_all_pass'] else 'FAIL'}.") + +if __name__ == "__main__": + main() diff --git a/eval/design11/ws5_checkers_verify.py b/eval/design11/ws5_checkers_verify.py new file mode 100644 index 0000000..a4d9752 --- /dev/null +++ b/eval/design11/ws5_checkers_verify.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""WS5 (г) verification — deterministic defect-class checkers. + +For each checker: (a) POSITIVES — the known empirical defects (exp15 §7.9 / §2 traps) must be +caught; (b) FALSE-POSITIVE rate — firing on the FULL 25-chapter clean ru final. A checker that +flags a notable share of clean text does not land. + +Corpus: rerun/records.json {source, final} per chunk (25 chapters). The prod FP measurement +(after build) runs through `tmctl export` (invariant #8); the rerun store is schema v7 vs HEAD +v8 (read-only cannot migrate), and exportNormalize is a cosmetic width-fold that does not alter +the words these checkers key on, so records.json final is a faithful $0 pre-build FP corpus. + +$0: deterministic, no LLM, no network. Reuses eval/exp15/q4a_traps rule_b1/rule_b2 machinery. +Writes /home/ubuntu/books/gu-zhenren/design11/ws5_checkers_verify.json (OUT of git). +""" +import json, os, sys, re + +sys.path.insert(0, "/home/ubuntu/projects/textmachine/eval/exp15") +import q4a_traps as Q # rule_b1, rule_b2, sentence_with, LOCATOR + +RECORDS = "/home/ubuntu/books/gu-zhenren/rerun/records.json" +OUT = "/home/ubuntu/books/gu-zhenren/design11/ws5_checkers_verify.json" + +# ---------- DC-1: 时辰 (double-hour) unit checker ---------- +_CN_NUM = {"一":1,"二":2,"两":2,"三":3,"四":4,"五":5,"六":6,"七":7,"八":8,"九":9,"十":10} +def cn_num(s): + if s.isdigit(): + return int(s) + return _CN_NUM.get(s) +_SHICHEN = re.compile(r"([0-9一二两三四五六七八九十])\s*个?\s*时辰") +_RU_HOURS = re.compile(r"(\d+|один|два|двух|три|трёх|трех|четыре|пять|шесть)\s+час") +def shichen_checker(src, tgt): + m = _SHICHEN.search(src) + if not m: + return None + n = cn_num(m.group(1)) + if n is None: + return None + expected_hours = n * 2 # 1 时辰 = 2 modern hours + # find an hours rendering in target; flag ONLY on an explicit mismatch (count rendered as hours). + # A paraphrase with no explicit hours count is a valid rendering, NOT a defect (refinement: the + # "no hours found" branch false-flagged ch3/chunk0 at 1.8% — dropped). + hm = _RU_HOURS.search(tgt) + if not hm: + return {"flag": False} + tok = hm.group(1) + ru_num = {"один":1,"два":2,"двух":2,"три":3,"трёх":3,"трех":3,"четыре":4,"пять":5,"шесть":6}.get(tok) + if tok.isdigit(): + ru_num = int(tok) + if ru_num == n and ru_num != expected_hours: + return {"flag": True, "reason": f"{n}个时辰 rendered as {ru_num} часов (count) instead of ~{expected_hours}"} + return {"flag": False} + +# ---------- DC-2: number-scale magnitude checker (千万 / 数十万 + b1/b2 fractions) ---------- +_MAG = [("千万", 10_000_000, r"(десят\w* миллион|10\s*000\s*000|10000000)"), + ("数十万", 100_000, r"(сотн\w* тысяч|нескольк\w* сот\w* тысяч|[1-9]00\s*000)")] +def magnitude_checker(src, tgt): + flags = [] + for token, value, ok_re in _MAG: + if token in src: + if not re.search(ok_re, tgt, re.I): + # positive iff a WRONG smaller magnitude is rendered + if token == "千万" and re.search(r"тысяч", tgt) and not re.search(r"миллион", tgt): + flags.append(f"{token}=10M rendered as 'тысячи' (10000x under)") + elif token == "数十万" and re.search(r"десятк\w* тысяч", tgt): + flags.append(f"{token}~several×100k rendered as 'десятки тысяч' (10x under)") + # fraction/percent via q4a rules on the located sentence + for cls, rule in (("b1", Q.rule_b1), ("b2", Q.rule_b2)): + loc = Q.LOCATOR.get(cls) + if loc: + sent = Q.sentence_with(tgt, loc) + if sent: + v = rule(sent) + if v == "fail": + flags.append(f"{cls}: numeric magnitude/fraction inverted in '{sent[:40]}...'") + return {"flag": bool(flags), "reasons": flags} if flags else {"flag": False} + +# ---------- DC-6: register-lexicon negative-list ---------- +NEG_LIST = {"терем", "терема", "тереме", "теремом", "терему", "теремах"} # сказочно-русский регистр в сянься +def register_checker(tgt): + low = tgt.lower() + hits = sorted({w for w in NEG_LIST if re.search(r"\b" + re.escape(w) + r"\b", low)}) + return {"flag": bool(hits), "hits": hits} if hits else {"flag": False} + +# ---------- DC-5: verse / allusion detector (source-side, structural) ---------- +# Poetic parallel lines: >=2 short clauses split by CJK comma with near-equal Han length, or +# known couplet markers; flag when target renders them as flat prose (no line breaks / all one sentence). +# A loose "short clauses" heuristic false-fires on 73.7% of prose (measured). Landable version +# requires BOTH structural regularity (>=3 clauses of EQUAL Han length 4-7) AND a poetic-register +# marker (classical imagery lexeme), so ordinary comma-listing prose does not trip it. +_VERSE_LEX = set("落日青丝暮雪朝明月清风山河湖海霜霞烟雨云天地花酒诗词赋") +def verse_checker(src, strict=True): + for line in src.split("\n"): + clauses = [c.strip() for c in re.split(r"[,、;]", line) if c.strip()] + if len(clauses) < 3: + continue + hlens = [sum(1 for ch in c if "一" <= ch <= "鿿") for c in clauses] + if not strict: + if len([h for h in hlens if 3 <= h <= 8]) >= 2 and max(hlens) <= 8: + return {"flag": True, "reason": f"loose: {len(clauses)} short clauses"} + continue + # strict: >=3 clauses of the SAME length in [4,7] (regular meter) AND a poetic lexeme present + from collections import Counter + common_len, cnt = Counter([h for h in hlens if 4 <= h <= 7]).most_common(1)[0] if any(4 <= h <= 7 for h in hlens) else (0, 0) + regular = cnt >= 3 + lex = any(ch in _VERSE_LEX for ch in line) + if regular and lex: + return {"flag": True, "reason": f"strict verse: {cnt} clauses len={common_len} + poetic lexeme"} + return {"flag": False} + +# ---------- DC-3: gender-enforce (hidden character before reveal) ---------- +# 白凝冰 = hidden; before until_ch the target must NOT use female surface forms for this character. +HIDDEN_NAME_RU = re.compile(r"Бай\s+Нинбин|Бай\s+Нин\s*Бин|白凝冰") +_FEMALE_FORM = re.compile(r"\b(она|её|ей|ею|неё)\b|\b\w+(ла|лась)\b") +def gender_hidden_checker(src, tgt): + if "白凝冰" not in src and not HIDDEN_NAME_RU.search(tgt): + return None + # flag if a female surface form co-occurs near the hidden character (pre-reveal window) + if HIDDEN_NAME_RU.search(tgt): + window = tgt + if re.search(r"\b(она|её|ей|неё)\b", window): + return {"flag": True, "reason": "female pronoun for gender:hidden character before reveal"} + return {"flag": False} + +def main(): + recs = json.load(open(RECORDS)) + n_chunks = len(recs) + + # ---- (a) POSITIVES: synthetic trap fixtures from exp15 §7.9 / §2 (checker must catch) ---- + positives = { + "DC1_shichen": shichen_checker("他闭关了三个时辰。", "Он затворился на три часа."), # want flag: 三时辰=6h, rendered "три часа" + "DC2_qianwan": magnitude_checker("有千万条蛊虫。", "Там были тысячи гу-червей."), # want flag: 千万=10M as "тысячи" + "DC2_shushiwan": magnitude_checker("聚集了数十万人。", "Собрались десятки тысяч человек."), # want flag: 数十万 as "десятки тысяч" + "DC6_terem": register_checker("Он вошёл в высокий терем."), # want flag: терем + # DC5: the loose line-profile detector CATCHES the trap, but at 74% FP (measured below) it is + # NOT landable as a gate; the strict version drops FP to 42% but LOSES this positive => + # deterministic verse detection is unviable -> DC5 is a pack-curated locus + owner decision. + "DC5_verse": verse_checker("青山落日,长河渐隐,朝如青丝暮成雪。", strict=False), # caught (loose only) + "DC3_gender": gender_hidden_checker("白凝冰走了过来。", "Бай Нинбин подошла, её взгляд был холоден."), # want flag: female pre-reveal + } + positives_caught = {k: (v is not None and v.get("flag", False)) for k, v in positives.items()} + + # ---- (b) FALSE-POSITIVE rate on the 25-ch clean final ---- + fp = {c: 0 for c in ("DC1_shichen", "DC2_magnitude", "DC6_register", "DC5_verse_strict", "DC5_verse_loose", "DC3_gender")} + fp_detail = {c: [] for c in fp} + for r in recs: + src = r.get("source", "") or "" + fin = r.get("final", "") or "" + ch, ci = r.get("chapter"), r.get("chunk_idx") + for name, fn, arg in ( + ("DC1_shichen", shichen_checker, (src, fin)), + ("DC2_magnitude", magnitude_checker, (src, fin)), + ("DC6_register", register_checker, (fin,)), + ("DC5_verse_strict", lambda s: verse_checker(s, strict=True), (src,)), + ("DC5_verse_loose", lambda s: verse_checker(s, strict=False), (src,)), + ("DC3_gender", gender_hidden_checker, (src, fin)), + ): + res = fn(*arg) + if res is not None and res.get("flag"): + fp[name] += 1 + if len(fp_detail[name]) < 5: + fp_detail[name].append({"ch": ch, "chunk": ci, "why": res}) + + report = { + "n_chunks": n_chunks, + "positives_caught": positives_caught, + "positives_all_caught": all(positives_caught.values()), + "fp_fire_counts_on_25ch_final": fp, + "fp_fire_rate_pct": {k: round(100 * v / n_chunks, 1) for k, v in fp.items()}, + "fp_detail_sample": fp_detail, + "note": ("FP fires are inspected: DC5 verse and DC6 register are the FP-prone target-side " + "checkers; DC1/DC2/DC3 are src<->target conversions with structurally low FP. " + "A checker flagging a notable share of clean text is refined before landing."), + } + os.makedirs(os.path.dirname(OUT), exist_ok=True) + json.dump(report, open(OUT, "w"), ensure_ascii=False, indent=2) + print(json.dumps({k: report[k] for k in ("n_chunks", "positives_caught", "positives_all_caught", + "fp_fire_counts_on_25ch_final", "fp_fire_rate_pct")}, ensure_ascii=False, indent=2)) + print("\nFP detail sample:") + for k, v in report["fp_detail_sample"].items(): + if v: + print(f" {k}: {v[:2]}") + print(f"\nVERDICT: positives {'ALL CAUGHT' if report['positives_all_caught'] else 'MISSED SOME'}; " + f"FP rates on 25-ch final: " + ", ".join(f"{k}={report['fp_fire_rate_pct'][k]}%" for k in fp)) + +if __name__ == "__main__": + main()