textmachine/docs/prompts/done/BACKEND_SESSION_PROMPT_CHUNKER.md

26 KiB
Raw Blame History

Промт сессии «Бэкенд — Шаг 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.mdD2 (диспозиция), 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.gochunkerVersion = "chunker-v2-ff-para-pack" (бампаешь), EstimateTokens (детерминированная токен-оценка — используй для «~12k токенов»), NormalizeSource, msgsContentHash, RequestHash.
    • backend/internal/pipeline/runner.goTranslateBook (грузит источник: os.ReadFileNormalizeSourceSplitChunks; ты вклиниваешь сюда 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.goBook.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/zipMETA-INF/container.xml → путь OPF → OPF spine (порядок itemref) + manifest (idhref) → читать каждый 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: Ingestchunks := 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.