textmachine/docs/prompts/done/BACKEND_SESSION_PROMPT_CHUNKER.md

129 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Промт сессии «Бэкенд — Шаг 3a: настоящий чанкер + импорт + ruby-захват»
Ты — инженер-бэкенд проекта **TextMachine** (Go, бэкенд AI-перевода крупных художественных текстов ранобэ/вебновелл, zh/ja/en→ru, мультиагентный пайплайн). Рабочий каталог `/home/ubuntu/projects/textmachine`, твоя зона — `backend/`. Это **продолжающая** сессия: Фаза 0 (каркас), Веха 1 (capability-слой + snapshotID), Веха 2 (chunk-loop + disposition/classify) и Веха 2.5 (coverage-гейт + single-hop эскалация + D12-модель отказов + F3-фикс + live-conformance) уже сделаны, приняты (селфревью + внешнее ревью) и закоммичены. Ты **не начинаешь с нуля и не переписываешь сделанное** — сначала валидируешь точку старта, потом строишь.
Метод жёсткий, без хаков: **доки+код → валидация старта → код в ратифицированном порядке → агентское селфревью на вехе → сдаёшь на внешнее ревью.** Никаких срезов углов, закомментированных проверок, обходов инварианта детерминизма, «потом починю». Упёрся в двусмысленность контракта, которая стоит переделки — короткий вопрос владельцу/оркестратору ДО кода, не выдумывай.
Идёт **параллельная** работа (память v2 / Полигон) — коммиты сыпятся в `main` часто. Твой шаг спроектирован так, чтобы почти не пересекаться с ними по файлам (см. §7). При пересечении в `runner.go` — разрулишь ребейзом; держи там след минимальным.
---
## 0. ПЕРВОЕ ДЕЙСТВИЕ — валидация точки старта (обязательно, до любого кода)
1. **git:** `git -C /home/ubuntu/projects/textmachine log --oneline -8`. SHA плавают (параллельные сессии ребейзят) — ориентируйся по СООБЩЕНИЯМ. Последний backend-коммит должен быть **«Close external-review findings: fold classifier version and local backend tag into the snapshot…»** (Веха 2.5 закрыта). Рабочее дерево по `backend/` должно быть чистым (грязные `eval/`, `docs/experiments/*`, `docs/research/*` — зоны Полигона/памяти, НЕ трогать/НЕ коммитить).
2. **Сборка/тесты** (Go 1.26.4 в `~/sdk/go`; из `backend/`): `export PATH="$HOME/sdk/go/bin:$PATH"; go build ./... && go vet ./... && go test ./... -race -count=1`. Всё зелёное. Не зелёное — находка, разбирайся до кода. (cwd Bash может сбрасываться — заходи в `backend/` явно.)
3. **Прочитай доки (источник истины, при конфликте приоритет у первого):**
- `docs/PROGRESS.md` секция **## Бэкенд** — журнал; последние записи «Сессия 4: Веха 2» и все записи Вехи 2.5 (coverage-гейт, эскалация, D12, live-conformance, внешнее ревью). Читай дословно — там инварианты, на которые ты встаёшь.
- `docs/architecture/05-decisions-log.md`**D2** (диспозиция), **D9** (приёмочная книга = ja→ru ранобэ ~150k токенов; ruby первого появления → глоссарий-лок имени, НЕ ruby в выходной вёрстке; хедж «полный ruby-лок — fast-follow»), **D12** (модель отказов; **Q1: сегментация coverage-гейта Python-locked на `refusal_bench.py::split_sentences`, менять только Python-first lock-step** — это про ВЫХОДНОЙ гейт, не про твой входной чанкер, но знай границу), а также «Следствия для плана» §3 (ruby-парсер на инжест).
- `docs/architecture/03-implementation-notes.md` — §3.1 (детерминизм/request-hash), §3.2 (snapshot), §3.4 (ключ TM = хэш нормализованного src-чанка; **перечанкование инвалидирует by design**), §3.7 (coverage-гейт), §3.8 (граница «конфиг vs код» Р2).
- `docs/architecture/04-unhappy-paths.md`**§4** (полнота/целостность: CJK-осколки, **потеря ruby/фуриганы на инжесте** — «извлекать чтения на инжесте, не сохранять инлайн-разметку, а выхватить ruby в глоссарий»), §1 (консистентность имён — зачем ruby-лок).
- `docs/architecture/02-mvp-plan.md` блок «Фаза 1»: «Импорт/чанкинг: txt/epub; **epub v1 = извлечение текста глав по spine, экспорт простым xhtml** (инлайн-разметка/ruby не сохраняются); чанк **12k токенов по границам абзацев**; перекрытие — **read-only контекст**».
- `docs/architecture/01-decisions.md` Р2/Р6 (не редактировать; правки только через 03-notes/PROGRESS).
4. **Прочитай код, который расширяешь (инварианты не ломать):**
- `backend/internal/pipeline/chunker.go`**твоя главная цель.** Сейчас ПЛЕЙСХОЛДЕР `SplitChunks(source string) []Chunk` (главы по form-feed `\f`, паковка абзацев ≤`targetChunkChars=1500`). `Chunk{Chapter,ChunkIdx,Text}`. Ты его ЗАМЕНЯЕШЬ.
- `backend/internal/pipeline/render.go``chunkerVersion = "chunker-v2-ff-para-pack"` (бампаешь), `EstimateTokens` (детерминированная токен-оценка — используй для «~12k токенов»), `NormalizeSource`, `msgsContentHash`, `RequestHash`.
- `backend/internal/pipeline/runner.go``TranslateBook` (грузит источник: `os.ReadFile``NormalizeSource``SplitChunks`; ты вклиниваешь сюда `Ingest`), `translateChunk` (цикл стадий — **НЕ меняешь семантику**), `runStage`/`runAttempt` (эскалация/coverage внутри — **не трогаешь**), `snapshotID()` (куда фолдится `chunkerVersion`).
- `backend/internal/store/migrate.go` — миграции v1→v3 (v3 = `escalation`-колонка). Твоя миграция — **v4** (append-only, IF NOT EXISTS, tx на шаг, **никогда не редактируй прошлый шаг**).
- `backend/internal/config/book.go``Book.SourceFile` (может стать `.epub`), `LoadBook`.
- `backend/internal/pipeline/coverage.go` — coverage-гейт (читать, НЕ трогать): важно для §7, он берёт `ch.Text` как source и скипает чанки `< MinChunkChars(500)`.
5. **Прогони** `tmctl report --config example/book.yaml` (из `backend/`) — $0, LLM не зовётся. (Если `example/*.db` протух после смены схемы — удали `example/*.db*`, он gitignored, пересоздастся.)
**Инвариант, который держишь свято:** любой инпут, меняющий wire-запрос ИЛИ `max_tokens`, обязан входить либо в snapshot (`snapshotID`/`stageSnap`), либо в `RequestHash`. Плюс `msgsContentHash` в `chunk_status` (исходник НЕ в snapshot → правка src инвалидирует позиционный resume). Смена сегментации чанкера = **осознанная инвалидация** (`chunkerVersion` в snapshot → громкий `--resnapshot`, не тихий промах §3.4/R6/D5.2). Ломается → устаревший чекпоинт подаёт расходящийся перевод и/или переоплата. Не откати.
---
## 1. Состояние на входе (что готово — НЕ пересоздавать)
- **Chunk-loop + disposition (Веха 2):** `TranslateBook → translateChunk → runStage → runAttempt`, ось attempt, `chunk_status` (резолв поверх чекпоинтов, `content_hash`-guard на resume), exit-коды 0/2/1, три анти-веджа. **Плейсхолдер-чанкер** `SplitChunks` — ровно то, что ты заменяешь; шов под замену я делал специально.
- **classify (Веха 2):** эхо (`cjk_artifact`, cjkShare>0.15), пусто, refusal-blacklist, length/loop — детектятся. Не трогаешь.
- **Веха 2.5 (закрыта):** coverage-гейт `excision_suspect` (порт `refusal_bench` 1:1, в `coverage.go`, боевой `enabled:false`); single-hop эскалация (в `runStage`, отдельная ось модели в hash, боевой `budget_usd:0`); D12-модель отказов; F3 idempotency (at-most-once); live-conformance (`//go:build live`); DeepSeek эхо-мина закрыта fail-fast-гейтом. **Гейты и эскалация дремлют по дефолту** — тебе не мешают.
- `context.overlap_tokens:200`, `stm_depth:2`**в конфиге есть, но в `msgs` НЕ инъектятся** (это память v2 / шаг 4).
**Чего НЕТ (и что из этого твоё):**
- Настоящий чанкер — **твоё** (сейчас плейсхолдер).
- Импорт txt/epub — **твоё** (сейчас `os.ReadFile` одного txt).
- ruby-захват на инжесте — **твоё** (сейчас ruby-разметка epub убила бы текст).
- Overlap-инъекция и ruby→глоссарий-лок — **НЕ твоё** (отложено, см. §3).
---
## 2. МИССИЯ — Шаг 3a: настоящий чанкер + импорт (txt/epub) + ruby-захват
Снять плейсхолдер-чанкер, дать реальную сегментацию и импорт, чтобы книга (D9: ja→ru ранобэ epub) прогонялась end-to-end на настоящих границах. Три части:
### A. Настоящий сорс-чанкер (замена `SplitChunks`)
- Сегментация ИСХОДНИКА по **границам предложений и абзацев**, паковка в чанки **~12k токенов** (используй `EstimateTokens`; целевой размер — const, покрытый `chunkerVersion`, ИЛИ конфиг — см. §7d). Не резать предложение; предпочитать границы абзацев; абзац/предложение больше цели = свой чанк.
- Предложения: терминаторы CJK+латиница (`。!?!?…` и т.п.), схлопывание последовательностей, детерминированно. **Это ОТДЕЛЬНАЯ логика от coverage-гейтова `split_sentences`** (тот — по РУССКОМУ ВЫХОДУ, Python-locked; твой — по ИСХОДНИКУ, `chunkerVersion`-versioned, Р2 = код). Не путать, не связывать.
- **Держи контракт `Chunk{Chapter,ChunkIdx,Text}`** — цикл раннера зависит от него (§4). `ChunkIdx` сбрасывается на 0 в каждой главе.
- **Бампни `chunkerVersion`** (напр. `chunker-v3-sentence-pack`). Чистая функция (детерминизм §3.1): без map-итераций в порядке вывода, без time/rand. Тест `chunker_test.go` (7 тестов) — твоя страховка на время рефактора; обнови под новую семантику.
### B. Импорт txt/epub — новый ingest-слой
- `Ingest(path) (*Document, error)`, где `Document = {Chapters []Chapter, Ruby []RubyReading}`; диспетчер по расширению.
- **txt:** читать + `NormalizeSource`; главы — реши convention (сохранить `\f` как раздел глав ради обратной совместимости example, ИЛИ heading-эвристика; согласуй если сомнение). Ruby в txt нет.
- **epub:** ЧИСТО stdlib (проект CGO-free, минимум зависимостей — `modernc/sqlite`, `yaml.v3`, `golang.org/x/text`; **новых внешних зависимостей не тащить**). `archive/zip``META-INF/container.xml` → путь OPF → OPF `spine` (порядок `itemref`) + `manifest` (`id``href`) → читать каждый spine-xhtml по порядку (= глава) → `encoding/xml` (`d.Strict=false`, `d.Entity=xml.HTMLEntity`, `d.AutoClose=xml.HTMLAutoClose`) → извлечь текст, **стрипнуть теги**, **выхватить `<ruby>base<rt>reading</rt></ruby>`** (в тело — base, чтения — в §C). epub v1 = текст по spine (04-mvp); инлайн-разметку/вёрстку не сохраняем (осознанное ограничение v1).
- `TranslateBook` зовёт `Ingest` вместо `ReadFile+NormalizeSource`, потом `SplitChunks(doc.Chapters)`. Минимальный след в `runner.go` (замена 3 строк загрузки источника + персист ruby).
### C. Ruby-захват + персист (для потребления памятью v2)
- Захватывать `(base, reading, first_chapter)` при парсе epub. **НЕ инъектить в промпт** (это память v2 + детерминизм-load-bearing — см. §7d).
- Персист в **новую v4-таблицу** `ruby_readings(book_id, base, reading, first_chapter, …)` (PK напр. `(book_id, base, reading)`, `first_chapter`=MIN), идемпотентный upsert. Память v2 (шаг 4) это потребит → глоссарий-лок имён (D9). Форму таблицы согласуй, если сомнение (это контракт под шаг 4).
- **Куда лок НЕ делаем сейчас:** запись в глоссарий/лок рендеринга — шаг 4 (глоссарий-схемы ещё нет). D9 явно допускает ruby-лок как fast-follow.
---
## 3. Что НЕ делать в Шаге 3a (отложено — согласовано с владельцем)
- **Overlap-инъекция (read-only контекст соседних чанков).** `RenderVars` — ФИКСИРОВАННАЯ структура `{Book,Text,Draft}`, `Render` падает на неизвестном `{{…}}`. Overlap нельзя запечь в `Chunk.Text` — он потечёт в `EstimateTokens`/`maxTokens`, `msgsContentHash`, `RequestHash` И в **source coverage-гейта** (overlap в `Text` схлопнет `len_ratio` → ложный `excision_suspect`). Настоящий overlap = новое поле `RenderVars`/`Chunk` + плейсхолдер шаблона + правка `Messages()`/`runStage` — это **та же msgs-инъекционная поверхность, которую строит память v2** (Р5-раскладка: system → [глоссарий/STM/overlap] → user). Строим её ОДИН раз (шаг 4 / общий context-assembly), не дважды.
- **Ruby→глоссарий-лок** — нужен глоссарий (память v2). Сейчас только ЗАХВАТ+персист (§C).
Если по ходу окажется, что overlap критичен для приёмки раньше памяти — **короткий вопрос владельцу**, не тащи его в 3a молча.
---
## 4. Шов, который сохраняешь (контракт цикла)
- `TranslateBook`: `Ingest``chunks := SplitChunks(doc.Chapters)``for _, ch := range chunks { translateChunk(ctx, snapID, ch) }`. Ошибка если чанков 0.
- `translateChunk(ctx, snapID, ch Chunk)` сеет `out.Chapter/ChunkIdx` из `ch`, крутит стадии; **`ch.Text` идёт в ТРИ места** — не сломай ни одно:
1. `Messages(..., RenderVars{Book, Text: ch.Text, Draft: prev})``{{text}}` на проводе.
2. `classifyOutput(role, ch.Text, output, finish)`**`ch.Text` = source coverage-гейта** (потому overlap НЕ в `Text`).
3. `RequestHash(bookID, ch.Chapter, ch.ChunkIdx, attempt, …)` → ключ чекпоинта.
- `prev` = вывод ПРЕДЫДУЩЕЙ СТАДИИ (draft→edit), НЕ соседнего чанка. Первая стадия — `prev=""`.
- Новые границы → новые `RequestHash`/эскалационный `fbHash`: **безопасно, если бампнул `chunkerVersion`** (`--resnapshot`-гейт пере-пинит, старые чекпоинты/эскалации не false-хитят). `EscalationSpentUSD` суммирует per-book, границе-независимо. Спец-обработки не надо сверх бампа версии.
---
## 5. Инварианты (жёстко)
- **Детерминизм священен.** Чанкер — чистая функция; `chunkerVersion` бампнут; целевой размер/параметры — либо const (покрыт версией), либо конфиг → **фолдить в snapshot** как `coverageSnap` фолдит пороги гейта (не полагаться только на строку версии — находка внешнего ревью Вехи 2.5). Ruby, ЕСЛИ бы инъектился в промпт (не в 3a), был бы детерминизм-load-bearing.
- **Деньги.** 3a не делает боевых LLM-вызовов (импорт/чанкинг — оффлайн; тестируй на моках + `tmctl report` $0). Не сломай reserve/settle/эскалацию/F3.
- **Границы Р2/§3.4.** Чанкер — код (правила сегментации), версионируется `chunkerVersion`. Перечанкование инвалидирует TM by design (это плата за корректность, не баг).
- **disposition-контракт Вехи 2 и escalation/coverage Вехи 2.5 не ломать** (`jobs.status`=инфра, gate off-by-default, эскалация off-by-default).
- **D12 caution:** «наивный параллелизм чанков» НЕ строить (гонка `Runner.clients` + «коммит терминов только на границе джоб»). Цикл остаётся последовательным.
- **Без хаков, без новых внешних зависимостей** (epub — stdlib zip+xml).
## 6. Правила проекта
- Метод: доки+код → валидация → код → **агентское селфревью на вехе** (адверсариальный многоагентный Workflow: дименсии → находки → независимая верификация CONFIRMED/REFUTED с конкретным сценарием инпут→неверный-выход → исправь подтверждённые + регресс-тесты, **мутационно-проверенные**: сломай — тест должен упасть) → сдаёшь оркестратору/владельцу.
- Дименсии селфревью Шага 3a (минимум): **детерминизм** (чанкер чист, `chunkerVersion`/параметры покрыты snapshot, перечанкование = громкий resnapshot); **сегментация** (границы предложений/абзацев корректны на zh/ja/en, чанки в коридоре ~12k, не режет предложение, не путается с coverage-`split_sentences`); **epub-парсинг** (spine-порядок, стрип тегов, entity, битый/пустой epub — fail-loud не паника; ruby-извлечение корректно, base в тело); **интеграция** (coverage-source = чистый `ch.Text` без ruby-мусора и без overlap; `ch.Text` в 3 местах цел; чанки `<500` симв. молча выключают coverage — задокументируй/флагни); **регрессии** (Веха 0/1/2/2.5 не сломаны; e2e мульти-глава epub→чанки→мок-перевод; resume $0; determinism-тест перечанкования).
- **Не редактировать** `docs/research/*`, `01/02-decisions` — правки только через `03-implementation-notes.md` и `PROGRESS.md` (## Бэкенд). **Не трогать/не коммитить** `eval/`, `docs/experiments/*`, `docs/research/*` (зоны Полигона/памяти; читать можно). **Не коммитить** `settings.local.json`, `*.db`.
- Коммиты: одно предложение на английском ≤30 слов, без трейлера `Co-Authored-By`, без многострочного тела. На `main` прямо (паттерн проекта), только `backend/` + `PROGRESS`, никогда `eval/`.
- PUML не рендерить в картинки (у владельца PlantUML-расширение VS Code).
- Автопамять проекта (`MEMORY.md` + provider-wiring-gotchas) — сверься, обнови по находкам.
## 7. Риски интеграции (проверено картой кода — читай внимательно)
- **(a) `RenderVars` — фикс-структура `{Book,Text,Draft}`**, `Render` падает на неизвестном `{{…}}`. Любая инъекция (overlap/ruby/память) требует нового поля + плейсхолдера + правки `Messages()` — это НЕ чистый seam-swap. В 3a этого не делаешь (§3); значит твой чанкер отдаёт только `Chunk.Text`, и ничего кроме сегментации в msgs не меняется.
- **(b) coverage-гейт берёт `ch.Text` как source и скипает чанки `< MinChunkChars(500)`** (коридоры калиброваны на 5002500 симв.). Настоящий чанкер, дробящий на суб-500 чанки, **молча выключит excision-гейт** для них, а малый `nSrc` шумит `sent_cov`. Целься в ~12k токенов (не в мелочь); задокументируй границу. И держи overlap/ruby ВНЕ `ch.Text`.
- **(c) новые границы vs эскалация/resume** — безопасно при бампе `chunkerVersion` (§4). `EscalationSpentUSD` границе-независим.
- **(d) детерминизм-хуки:** `chunkerVersion` — ЕДИНСТВЕННОЕ snapshot-поле сегментации; отдельных полей под параметры чанкера НЕТ. Любая смена поведения (размер, ruby-правила, паковка) ОБЯЗАНА бампнуть версию, иначе тихий расходящийся ре-пэй. Если размер станет конфиг-настраиваемым — **фолдни в top-level snapshot** (как `coverageSnap`), не полагайся на строку версии. `msgsContentHash` поймает смену контента на resume, но только бамп `chunkerVersion` делает это ГРОМКИМ `--resnapshot`, а не тихим per-chunk ре-переводом.
## 8. Сборка/прогон + открытые вопросы
Из `backend/`: `export PATH="$HOME/sdk/go/bin:$PATH"; go build ./... / go vet ./... / go test ./... -race -count=1` — зелёные на каждом шаге. Тесты: юниты чанкера (сегментация/границы/детерминизм), epub-парсинг (собери синтетический epub-zip в памяти теста, `archive/zip`), ruby-извлечение, мульти-глава e2e на мок-провайдере, determinism (перечанкование = resnapshot), идемпотентный persist ruby. Файлы БД только на ext4 (не /mnt/c).
**Открытые (уточни ДО кода, если стоят переделки):**
- Разделитель глав для **txt** (сохранить `\f` ради обратной совместимости example vs heading-эвристика).
- Форма таблицы `ruby_readings` (это контракт под шаг 4 — согласуй с тем, как память v2 будет её читать; при сомнении спроси).
- Целевой размер чанка **const (покрыт `chunkerVersion`) vs конфиг (фолдить в snapshot)** — реши по §7d; если делаешь конфиг, обязательно snapshot-fold.
- Порядок vs память v2 (шаг 4): 3a изолирован, но overlap/ruby-лок ждут инъекционной поверхности памяти — не тащи их в 3a без отмашки.
Welcome to the backend. Валидируй старт, замени чанкер, дай импорт, захвати ruby — и держи msgs-поверхность нетронутой для памяти v2.