26 KiB
Промт сессии «Бэкенд — Шаг 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. ПЕРВОЕ ДЕЙСТВИЕ — валидация точки старта (обязательно, до любого кода)
- 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/*— зоны Полигона/памяти, НЕ трогать/НЕ коммитить). - Сборка/тесты (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/явно.) - Прочитай доки (источник истины, при конфликте приоритет у первого):
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 не сохраняются); чанк 1–2k токенов по границам абзацев; перекрытие — read-only контекст».docs/architecture/01-decisions.mdР2/Р6 (не редактировать; правки только через 03-notes/PROGRESS).
- Прочитай код, который расширяешь (инварианты не ломать):
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(детерминированная токен-оценка — используй для «~1–2k токенов»),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).
- Прогони
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_bench1: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)
- Сегментация ИСХОДНИКА по границам предложений и абзацев, паковка в чанки ~1–2k токенов (используй
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 → OPFspine(порядок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идёт в ТРИ места — не сломай ни одно:Messages(..., RenderVars{Book, Text: ch.Text, Draft: prev})→{{text}}на проводе.classifyOutput(role, ch.Text, output, finish)→ch.Text= source coverage-гейта (потому overlap НЕ вText).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, чанки в коридоре ~1–2k, не режет предложение, не путается с 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)(коридоры калиброваны на 500–2500 симв.). Настоящий чанкер, дробящий на суб-500 чанки, молча выключит excision-гейт для них, а малыйnSrcшумитsent_cov. Целься в ~1–2k токенов (не в мелочь); задокументируй границу. И держи 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.