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

This commit is contained in:
Claude (backend session) 2026-07-19 04:03:39 +03:00
parent 766a60e304
commit 09ca24b50a
14 changed files with 2384 additions and 4 deletions

View file

@ -0,0 +1,179 @@
# Промт: сессия БЭКЕНД — единый пак по плану 11 (волновой раннер · чанкер/единицы · банк W1.5 · банкнота · чекеры · армы), 2026-07-19
> **Место в очереди (пост-D39.12):** исследовательская программа арх-ресета (D39D39.10) и дизайн-синтез
> (D39.11) ЗАВЕРШЕНЫ. План **`docs/architecture/11-implementation-plan.md` РАТИФИЦИРОВАН (D39.12)** после трёх
> рубежей верификации ($0-симуляции с числами — исполнены и воспроизводимы). Эта сессия — СТРОЙКА строго по
> нему. **Решение владельца: весь объём — ОДНОЙ сессией**, четырьмя блоками с чекпойнт-ревью между.
## Кто ты и что сдаёшь
Бэкенд-сессия TextMachine. **Зона записи: `backend/`** (вкл. `configs/`, `prompts/`); всё остальное читать
можно, править нельзя (расхождения — пингом через отчёт/владельца). **НЕ коммитить** — лендит оркестратор
после приёмки (git-гардрейл CLAUDE.md: никаких reset --hard/rebase/checkout поверх грязного дерева). Сдаёшь:
1. Код WS1WS6 по плану + тесты из (д)-секций (пины/golden/race/kill-9).
2. **`backend/PACK11_REPORT.md`** — per-буллет чек-лист (✓/девиация+причина), тест-логи, golden-диффы,
раздел «багфиксы вне плана», раздел «структурные переносы» (если рефакторил), residual.
## Онбординг (порядок чтения, ~45 мин)
1. `CLAUDE.md` (гардрейлы: `.env` НЕ читать · 18+ уровень 3 · PUML не рендерить · книга и производные ВНЕ git)
`backend/README.md` (**инварианты 18 — ломать нельзя, каждый закреплён тестами**) →
`docs/architecture/03-implementation-notes.md`.
2. **`docs/architecture/11-implementation-plan.md` ЦЕЛИКОМ — это твоя спека.** Структура: WS1WS6, каждый
(а) что менять / (б) алгоритм / (в) конфиг+снапшот / (г) исполненная $0-верификация с числами /
(д) тест-спека ПОСЛЕ стройки / (е) открытое; §7 порядок · §8 манифест resnapshot · §9 диспозиции ·
§10 развилки владельца · §11 гейтнутые армы. План верифицирован трижды — **не пере-решай его решения**;
несогласие или найденная дыра = эскалация запиской, не тихая девиация.
3. `docs/architecture/05-decisions-log.md`: карта актуальности в шапке + D39D39.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)
- **Рефакторинг/перенос кода по папкам — РАЗРЕШЁН и приветствуется:** архитектурная чистота важнее
минимальности диффа. Границы: инварианты 18, контракты плана и поведение вне скоупа не менять молча;
крупные структурные переносы — отдельным разделом отчёта (ревью-абельность лендинга).
- **Без велосипедов:** реюз 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 → пере-прогон 310 глав → чтение владельца (планка 2 претензии).

View file

@ -4,11 +4,15 @@
> - **Фазы:** 0 ✅ · Ф1-инфра ✅ (D20D28; 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+F1F9 (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+волны W0W3 (D39.1; конфликт exp14↔research/18 разрешён: эмиссия≠окно≠связность) · research/20 банк-майнинг W1.5 (D39.6; детектор-лестница V-A/B/C, алиас-ярусы, banknote-v1, плагины P1P6, §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) → пере-прогон 310 глав → чтение владельца (интерим-планка 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) → пере-прогон 310 глав (армы редактора 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: мини-голд алиасов (~2030 мин) / бюджет ≤$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 оркестратора (F1F11: 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`** — единый пак WS1WS6 блоками A/B/C/D с чекпойнт-ревью (решение владельца: одной сессией; свободы: рефакторинг/перенос кода, временные тесты, багфиксы со скоуп-пометкой; Go-каноны; даблчек неконсистентностей; $0 к провайдерам). Очередь: бэкенд-сессия → приёмка (рубеж-2) → единый resnapshot → пере-прогон → чтение.
## Оркестратор №7 — промт ДИЗАЙН-СИНТЕЗ сессии выдан (deliverable №1 по D39.11), 2026-07-19
Онбординг по хендоффу №5 (D-лог D39D39.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 L1L8; $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-блок) → бэкенд-промт строго по плану.

View file

@ -12,11 +12,11 @@
## Структура
- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1D39.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/` — фактура исследований 0405.07: `0110` базовые, `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)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -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` перепроверить.
- **Таймауты**: книжные чанки на локали занимают **63278 с** (не 45 с как в чат-vojo). Нужен per-роль лег-таймаут ~300600 с + стриминг как 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.)*
## Календарь деприкаций / цен (мониторить ежеквартально)

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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=<majority banknote 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()

View file

@ -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()

View file

@ -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()