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:
parent
766a60e304
commit
09ca24b50a
14 changed files with 2384 additions and 4 deletions
179
docs/BACKEND_PLAN11_SESSION_PROMPT.md
Normal file
179
docs/BACKEND_PLAN11_SESSION_PROMPT.md
Normal file
|
|
@ -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 претензии).
|
||||
|
|
@ -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-блок) → бэкенд-промт строго по плану.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
1069
docs/architecture/11-implementation-plan.md
Normal file
1069
docs/architecture/11-implementation-plan.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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.)*
|
||||
|
||||
## Календарь деприкаций / цен (мониторить ежеквартально)
|
||||
|
||||
|
|
|
|||
239
eval/design11/ws1_wave_cogs.py
Normal file
239
eval/design11/ws1_wave_cogs.py
Normal 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()
|
||||
176
eval/design11/ws2_chunk_sim.py
Normal file
176
eval/design11/ws2_chunk_sim.py
Normal 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()
|
||||
132
eval/design11/ws2_fertility_verify.py
Normal file
132
eval/design11/ws2_fertility_verify.py
Normal 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()
|
||||
91
eval/design11/ws3_miner_verify.py
Normal file
91
eval/design11/ws3_miner_verify.py
Normal 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()
|
||||
173
eval/design11/ws3_seeddelta.py
Normal file
173
eval/design11/ws3_seeddelta.py
Normal 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()
|
||||
119
eval/design11/ws4_banknote_verify.py
Normal file
119
eval/design11/ws4_banknote_verify.py
Normal 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()
|
||||
187
eval/design11/ws5_checkers_verify.py
Normal file
187
eval/design11/ws5_checkers_verify.py
Normal 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()
|
||||
Loading…
Add table
Reference in a new issue