textmachine/docs/BACKEND_SESSION_PROMPT_MEMORY.md

128 lines
32 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.

# Промт сессии «Бэкенд — Шаг 4: банк памяти v2 (глоссарий + инъекционная поверхность + горячий путь)»
Ты — инженер-бэкенд проекта **TextMachine** (Go, бэкенд AI-перевода крупных художественных текстов ранобэ/вебновелл, zh/ja/en→ru, мультиагентный пайплайн). Рабочий каталог `/home/ubuntu/projects/textmachine`, твоя зона — `backend/`. Это **продолжающая** сессия: Фаза 0 (каркас), Веха 1 (capability + snapshotID), Веха 2 (chunk-loop + disposition), Веха 2.5 (coverage-гейт + single-hop эскалация + D12) и **шаг 3a** (настоящий чанкер + импорт txt/epub + ruby-захват; принят внешним ревью, все 7 находок закрыты) сделаны и закоммичены. Ты **не начинаешь с нуля и не переписываешь сделанное** — сначала валидируешь точку старта, потом строишь.
Метод жёсткий, без хаков: **доки+код → валидация старта → код в ратифицированном порядке → агентское адверсариальное селфревью на вехе → сдаёшь на внешнее ревью.** Никаких срезов углов, закомментированных проверок, обходов инварианта детерминизма, «потом починю». Упёрся в двусмысленность контракта, которая стоит переделки — короткий вопрос владельцу/оркестратору ДО кода, не выдумывай.
Память v2 — **главный блокер качества перевода** (консистентность имён/терминов — жалоба №1 читателей, 04-unhappy §1). Это самый рискованный шаг: тихая деградация тут — не «пусто», а **ошибочная инъекция**, которой сильная модель послушно следует (уверенность растёт, точность падает — arXiv 2510.00829). Реестр рисков `architecture/06` — твоё ТЗ по механизмам защиты; читай его дословно.
---
## 0. ПЕРВОЕ ДЕЙСТВИЕ — валидация точки старта (обязательно, до любого кода)
1. **git:** `git -C /home/ubuntu/projects/textmachine log --oneline -8`. SHA плавают (параллельные сессии). Последний backend-код-коммит — **«Close step-3a external-review findings…»** (7 находок 3a закрыты: spine-reconciliation, UTF-8-нормализация, quote-depth, ruby full-replace, tiling-fuzz). Рабочее дерево по `backend/` чистое (грязные `eval/`, `docs/experiments|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/` явно; пример проекта — `backend/example/`, НЕ repo-root.)
3. **Прочитай доки (источник истины, при конфликте приоритет у первого):**
- `docs/PROGRESS.md` §**Бэкенд** (последние записи: шаг 3a + закрытие 7 находок) и §**Память** (журнал памяти-сессии — вердикт+реестр).
- **`docs/architecture/06-memory-risk-registry.md` — ЦЕНТРАЛЬНЫЙ вход, читай дословно.** §«Гейты для бэкенда перед фиксацией схемы store/глоссария v2» (что обязан закрыть до заморозки схемы), §«Контракт горячего пути» (НЕ FTS5, НЕ вектор — детерминированный multi-pattern матч), §«Ruby-seed контракт» (вход из 3a), таблицы AG (каждая строка — механизм или дыра со статусом ✅/🔶/❌).
- **`docs/research/13-memory-bank-validation.md` §«Честные слабые места рекомендаций»** — ОБЯЗАТЕЛЬНО. Два несущих НЕдоказанных места: **F1** (snapshot материализует байты инъекции vs store версионирует глоссарий) и **E1 post-check** (надёжность в русской морфологии невалидирована — склонение-осознанный матч шумит → ложные флаги → редакторы игнорируют → safety рушится). Померь post-check на русском ДО того, как доверять ему как жёсткому гейту.
- `docs/architecture/05-decisions-log.md`**D7** (схема памяти v1: `src,dst,type,aliases,gender,speech,decl,since_ch/until_ch,status,note` + alias-граф + `gender=hidden` + `translit_policy` + `first_person` + series-bible-lite = gender+ты/вы-журнал; `nickname_translation` в v1), **D8** (memory-компонент snapshotID = content-hash материализации, не version-counter), **D10** (знаменатель консистентности = ВСЕ approved-термины, порог 98% на детерминированном слое), **D5.1/D5.2** (проактивный `gender=hidden`; snapshotID покрывает память).
- `docs/architecture/03-implementation-notes.md` — §3.2 (snapshot: замороженная версия approved-глоссария; **STM в snapshot НЕ входит**; батч-подтверждение терминов только на границе джоб), §3.6 (SQLite: **FTS5 в горячий путь НЕ тащить**; кириллица нормализуется в Go), §3.7 (coverage-гейт), §3.8 (граница Р2: режим инъекции/бюджеты — конфиг; сборка/матч — код).
- `docs/architecture/04-unhappy-paths.md` §1/§2/§4 + «Следствия» п.2; `02-mvp-plan.md` «Фаза 1» (банк памяти + приёмка ≥98%); `01-decisions.md` Р3/Р5/Р6 (не редактировать).
4. **Прочитай код, который расширяешь (инварианты не ломать):**
- `pipeline/render.go`**`RenderVars{Book,Text,Draft}` — ФИКС-структура**, `Render` падает на неизвестном `{{…}}`; `Messages()`=`[system(CacheBoundary:true), user]`, **инъекция глоссария/STM встаёт МЕЖДУ ними** (комментарий в коде это фиксирует), Р5-раскладка. `memoryVersion()`**сейчас хеширует КОНСТАНТУ `tm-memory-v1\x00`** (заглушка; **твоя задача — заменить на хеш ORDER BY-стабильных ЗАМОРОЖЕННЫХ approved-строк**, F1). `RequestHash` включает контент `msgs`; `NormalizeSource`; `EstimateTokens`.
- `pipeline/runner.go``snapshotID()` уже сворачивает `contextSnap`(glossary_injection/budget/stm/overlap/ttl) + `MemoryVersion` (F1 структурно, 5eaf2d9 — активируется твоей материализацией); `translateChunk`/`runStage`/`runAttempt` (цикл стадий — **семантику НЕ меняешь**); `persistRuby` (шаг 3a — `ReplaceRubyReadings`, полная замена агрегата). Инъекция память→msgs встаёт в `runStage` перед `Messages()`.
- `pipeline/chunker.go`/`ingest.go` — источник `Chunk.Text` и `ruby_readings` (твой сид). Не трогаешь, потребляешь.
- `store/migrate.go` — миграции v1→v4 (v4 = `ruby_readings`). Твоя — **v5** (append-only, IF NOT EXISTS, tx на шаг, **никогда не редактируй прошлый шаг**). `store/ruby.go``RubyReadingsForBook` (сид auto-кандидатов).
- `config/pipeline.go``ContextAssembly`(glossary_injection `selective|full_prefix`, glossary_token_budget, stm_depth, overlap_tokens, cache_ttl) — схема есть, инъекции нет.
5. **Прогони** `tmctl report --config example/book.yaml` (из `backend/`) — $0, LLM не зовётся. (Протухший `example/*.db*` — gitignored, удали, пересоздастся — schema бампнута v4→после смены `chunkerVersion` пример пере-снапшотится.)
6. **Исполнимый эталон (зона Полигона — читать, НЕ редактировать/коммитить):** `eval/memory_hotpath.py` (T1T10 → Go-юниттесты), `eval/retrieval_bench.py`+`eval/data/retrieval_bench/` (точный многосимвольный подстрочный матч precision 1.0; фаззи/одиночные ключи — все ловушки в top-5), `eval/extract_bench.py` (спот=локаль OK, рендер dst=облако/человек).
**Инвариант, который держишь свято:** любой инпут, меняющий wire-запрос ИЛИ `max_tokens`, обязан входить либо в snapshot, либо в `RequestHash`. **Материализованная память (approved-глоссарий/резюме/series-bible на момент старта джобы) обязана войти в `memoryVersion()` → snapshot;** рендер читает **замороженную** версию, не live (F1 — 🔴🔇, класс D5.2). STM в snapshot НЕ входит (пересобирается из чекпоинтов).
---
## 1. Состояние на входе (что готово — НЕ пересоздавать)
- **Chunk-loop + disposition + coverage + escalation (Вехи 2/2.5), чанкер+ingest+ruby (3a, 7 находок закрыты)** — семантику не трогаешь, потребляешь.
- **snapshotID уже сворачивает `context_assembly` + `MemoryVersion` (F1 структурно, 5eaf2d9):** остаётся **заменить константу в `memoryVersion()` на хеш материализации** — иначе F1 тихо регрессирует.
- **`ruby_readings` (v4):** capture-only, полная замена агрегата (не в промпте/hash). **Твой сид auto-кандидатов** (не approved напрямую).
- **Config-ручки `context.*`** заведены, но в `msgs` НЕ инъектятся — твоя работа.
- **`RenderVars` — фикс-структура**, `Render` падает на неизвестном `{{…}}` (инъекция = НЕ template-плейсхолдер по умолчанию; см. §3).
---
## 2. МИССИЯ — банк памяти v2 (предлагаемая первая веха; полный скоуп согласуй в §8)
Дать пайплайну **детерминированную селективную инъекцию approved-глоссария в горячий путь + обязательный post-check**, построив инъекционную msgs-поверхность ОДИН раз (её же ждут отложенные overlap-инъекция и ruby-лок шага 3a). Ратифицированный порядок реестра (§«Гейты перед фиксацией схемы»):
### A. Схема store v2 (миграция v5) — заморозка глоссария
База D7 + **добавь из реестра:** поле `sense` (A3/полисемия); **ось «редакционное время» ОТДЕЛЬНО от `since_ch/until_ch`** (B1 stale — append-only журнал ревизий рендеринга); `UNIQUE(src,sense,window)` + обратная проверка `dst`-коллизий инъективности (B2); `min_key_len` per-язык + **запрет одиночного Han/каны/латинской буквы как самостоятельного ключа** (A3, schema-констрейнт); статус-машина термина (`auto|draft|approved` + журнал). **Нормализационный артефакт (A4, ❌ самый вероятный «тихо пусто»):** NFKC + OpenCC упрощ./трад. + kana-folding + захват ruby-чтений, **симметрично к ключам/алиасам И к тексту чанка**; версионированный, тестируемый, детерминированный. Кириллица нормализуется в Go до записи (§3.6).
### B. Горячий путь — детерминированный матчер (НЕ FTS5, НЕ вектор, НЕ эмбеддинги)
Multi-pattern матч (Aho-Corasick / double-array trie) по `src`+алиасам над **нормализованным** чанком; для ru/en — по леммам/`decl`. Longest-match, замена целой сущности. **«Лучше ничего, чем мусор» (A1):** пустой матч — норма/безопасно; бюджет допустимо недозаполнить; НЕ добивать соседями; **точность > полнота.** Sticky scene-inertia (A5: персонажи прошлых 12 чанков — состояние из детерминированной пересборки чекпоинтов, НЕ эмбеддинги). **Трёхсторонний disposition инъекции (A2, ❌):** `CONFIRMED`→авторитетно; `AMBIGUOUS` (короткий/общий алиас, коллизия поверхности, auto/draft)→инъекция с пометкой «unverified» **+ принудительный post-check**; `REJECT`→выкинуть+залогировать; каждая ветка наблюдаема. **Спойлер-окно `since_ch/until_ch` = жёсткий reject-гейт с логируемым событием (C1, safety, не UX-фича).** **`retrieval-state` per-chunk:** `n_exact_hits, n_sticky, n_ambiguous_flagged, n_spoiler_blocked, embedding_tier_used(=0 в v1)`.
### C. Инъекционная msgs-поверхность (строим ОДИН раз, Р5)
Раскладка `system(стабильный префикс, кэш-граница) → [глоссарий/STM] → user(волатильный хвост: чанк/черновик)` (A6: авторитетный блок глоссария рядом с чанком, не в середине стопки резюме). **Селективный глоссарий в волатильном хвосте не ломает DeepSeek-префикс-кэш** (Р5). Это **та же поверхность, что ждут отложенные overlap-инъекция и ruby→глоссарий-лок 3a** — строишь один раз, они встают на неё.
### D. memoryVersion() — закрыть F1
Замени константу на **хеш ORDER BY-стабильных ЗАМОРОЖЕННЫХ approved-строк** (версия approved-глоссария + резюме/series-bible на старте джобы). Рендер читает pinned-версию из snapshot, не live. Смена approved → громкий `--resnapshot`.
### E. Post-check — ОБЯЗАТЕЛЬНЫЙ гейт (E1/E2, не опциональный флаггер)
Для каждого approved-термина с `src` в чанке — проверить, что **склонённая** форма `dst` (через `decl`/лемматизатор) есть в выходе; нет → флаг/точечный ре-перевод. Ловит и «модель проигнорировала (1736% терминов)», и «мы инъектили неверное». **ЗАПРЕТ слепого post-replace форм глоссария в ru-выход (E2):** форс только когда термин морфологически инвариантен ИЛИ полный `decl` И падеж резолвится; иначе флаг (модели даём лемму, склоняет сама). `decl` — safety-механизм грамматичности. **⚠ Померь надёжность post-check на русском (E1, невалидирована) ДО жёсткого гейта** (порт `eval/memory_hotpath.py` T1T10 в Go); шумит → флаг-режим, не форс.
### Ruby-seed (вход из 3a, реестр §Ruby-seed, подтверждён оркестратором)
`base``src`; `first_chapter``since_ch`; `occurrences`→**приор уверенности** (НЕ approved напрямую). `reading`→**НЕ dst напрямую, а мост к Поливанов-dst-валидации** (усиливает B6). **Различай имя-чтение vs авторское двойное чтение:** 鈴木→すずき (name-lock, Поливанов) против 強敵→とも (значат РАЗНОЕ**сноска, не лок**; блайнд-лок = mistranslation); неоднозначное → человеку. Ruby — **СИД в auto-кандидатный пул**, промоушен в approved батч-судьёй/человеком (G1). (Ruby-хранилище 3a уже делает полную замену агрегата — фантомов нет.)
---
## 3. Шов, который сохраняешь (контракт цикла и рендера)
- **`RenderVars{Book,Text,Draft}` — фикс-структура; `Render` падает на неизвестном `{{…}}`.** Инъекция = НЕ template-плейсхолдер по умолчанию. Два чистых пути: (а) **код-собранный injection-message** в `Messages()` из матченных записей (предпочтительно — без правки шаблонов), ИЛИ (б) новое поле `RenderVars` + плейсхолдер + правка `Messages()`. Реши по Р5; кэш-граница остаётся на стабильном префиксе.
- **`ch.Text` в ТРЁХ местах (3a-контракт):** `{{text}}` на проводе, source coverage-гейта, `RequestHash`. Инъекция — ОТДЕЛЬНЫЙ блок, НЕ в `ch.Text` (иначе течёт в coverage len_ratio и хеш).
- **LLM только батч/граница джоб**, НИКОГДА в горячем пути инъекции (F3/§3.2). Цикл последовательный (D12: гонка `Runner.clients` + коммит терминов только на границе джоб — наивный параллелизм чанков НЕ строить).
- **Смена материализованной памяти → `memoryVersion()` → snapshot → громкий `--resnapshot`** (не тихий промах, F1).
## 4. Что НЕ делать в шаге 4 (отложено — реестр §5, не блокирует v1-схему; заложи поля/место)
- Эмбеддинги/вектор на горячем пути (A7/D2) — Фаза 2, low-trust; sqlite-vec не сейчас.
- Транслит-стандарты Палладий/Поливанов post-check (B6) — поле/место в v1, валидатор — Фаза 2 (reading как мост заложи).
- Гейт фактичности резюме (D1, ❌ первоклассный пробел), морфо-гейт глагольного рода (C3) — Python-сайдкар Фазы 2.
- Whole-book RAG по резюме (D2), бэкап/интеграл файла книги (F4) — Фаза 2.
- Автоэкстракция/адъюдикация конфликтов/батч-подтверждение (G1/B2/F5), резюме глава→арка→книга, series-bible-lite — если не влезает в первую веху, **отдельная веха** (спот=локаль / рендер dst=облако+человек; экстракция — LLM только на границе джоб, НЕ в горячем пути). Согласуй границу вехи (§8).
Если автопопуляция окажется критична для приёмки раньше — **короткий вопрос оркестратору**, не тащи молча.
## 5. Инварианты (жёстко)
- **Детерминизм священен.** Матч/инъекция/post-check/нормализация — чистые детерминированные функции; `memoryVersion()` = хеш замороженной материализации в snapshot; нормализация — версионированный артефакт; никаких map-итераций в порядке вывода, time/rand.
- **Тихую деградацию конвертируем в громкую, а не устраняем обещанием:** post-check + disposition-лог + retrieval-state. Пустой retrieval — норма; **ошибочная инъекция — главный враг** (A2).
- **Деньги.** Горячий путь — $0 (без LLM). LLM-экстракция/адъюдикация — батч на границе джоб, покрыта reserve/settle/эскалацией/F3 — не сломай.
- **Границы Р2/Р3.** Матч/сборка — код; режим инъекции/бюджеты/TTL — конфиг. Кириллица нормализуется в Go. FTS5/вектор — НЕ на горячий путь.
- **disposition/coverage/escalation Вех 2/2.5 не ломать.**
## 6. Правила проекта
- Метод: доки+код → валидация → код → **агентское селфревью на вехе** (адверсариальный многоагентный Workflow: дименсии → находки → независимая верификация CONFIRMED/REFUTED с конкретным сценарием инпут→неверный-выход → исправь подтверждённые + регресс-тесты **мутационно-проверенные**: сломай — тест должен упасть) → сдаёшь оркестратору/владельцу.
- **Дименсии селфревью шага 4 (минимум):** детерминизм (матч/инъекция/post-check/`memoryVersion` — F1 не регрессирует); нормализация/матч (A3/A4 — одиночные ключи запрещены, симметрия ключ↔текст, 0 ложных на ловушках `retrieval_bench`); disposition инъекции (A2 три ветки, AMBIGUOUS→post-check); спойлер (C1 жёсткий reject); post-check (E1/E2 — форс только при резолвимом падеже, слепой replace запрещён, русская морфология померена); интеграция (msgs-поверхность Р5, `ch.Text` чист, кэш-граница цела, snapshot-fold памяти); регрессии (Вехи 02.5 + 3a целы; e2e инъекция→перевод→post-check; resume $0; ре-снапшот при смене approved).
- **Не редактировать** `docs/research/*`, `01/02-decisions`, `06-memory-risk-registry` — правки только через `03-implementation-notes.md` и `PROGRESS.md` (§Бэкенд). **Не трогать/не коммитить** `eval/`, `docs/experiments|research/*` (читать можно). **Не коммитить** `settings.local.json`, `*.db`.
- Коммиты: одно предложение на английском ≤30 слов, без трейлера `Co-Authored-By`, без многострочного тела. На `main` прямо, только `backend/` + `PROGRESS`, никогда `eval/`. PUML не рендерить в картинки. Автопамять (`MEMORY.md`) — сверься.
## 7. Риски интеграции (проверено картой кода)
- **(F1) `memoryVersion()` — константа-заглушка.** Заменишь на хеш материализации — иначе смена approved-глоссария молча промахивается мимо resnapshot-гейта и переоплачивает расходящимся переводом (🔴🔇). Материализуй **замороженные** строки (pinned), не live.
- **(A4) нормализация — самый вероятный «тихо пусто».** Ключ есть в тексте, но в другой форме (упрощ./трад., полу/полноширина, кандзи↔кана, ruby). Симметричная нормализация ключа И текста — версионированный артефакт.
- **(A2/A3) ошибочная инъекция — главный источник тихой деградации.** Одиночный CJK/короткий алиас срабатывает на чужом смысле (阿Q, 炎=пламя vs имя 萧炎). Запрет одиночных ключей + longest-match + трёхсторонний disposition.
- **(E1) post-check в русской морфологии невалидирован.** Склонение-осознанный матч шумит. Померь ДО жёсткого гейта; шумит → флаг, не форс.
- **(Р5/кэш) инъекция между system и user** не должна сдвигать кэш-границу со стабильного префикса (иначе DeepSeek-автокэш промахивается, Anthropic `cache_control` не там).
## 8. Сборка/прогон + открытые вопросы
Из `backend/`: `go build/vet/test -race -count=1` — зелёные на каждом шаге. Тесты: нормализация (A4, версионированная), матчер (longest-match/одиночные запрещены/ловушки `retrieval_bench` 0 ложных), disposition (3 ветки), спойлер-reject, post-check (T1T10 порт + **русская морфология**), memoryVersion-материализация (смена approved → `--resnapshot`), e2e инъекция→мок-перевод→post-check, resume $0. Файлы БД только на ext4 (не /mnt/c).
**Открытые (уточни ДО кода, если стоят переделки):**
- **Граница первой вехи:** только AE (схема+горячий путь+инъекция+F1+post-check) на РУЧНОМ/ruby-сиде глоссарии, а автопопуляцию/адъюдикацию/резюме/series-bible — в следующие вехи? (Рекомендация: да — поверхность+post-check+F1 разблокируют качество и отложенные overlap/ruby-лок; популяция — отдельно.)
- **F1-развилка (research/13 §слабые места):** snapshot материализует БАЙТЫ инъекции vs store версионирует глоссарий-счётчиком. 5eaf2d9 выбрал материализующий вариант — следуй ему (хеш замороженных строк); подтверди, если сомнение.
- **Post-check как гейт vs флаггер** до замера русской морфологии (E1) — жёсткий гейт только после валидации precision.
- **Раскладка инъекции** (код-message vs RenderVars-поле) и кэш-граница Р5 — реши по §3, зафиксируй в PROGRESS.
- **Ruby: классификатор имя-чтение vs авторское двойное чтение** — детерминированное правило (фонетическая согласованность/паттерн имени) vs флаг человеку; согласуй порог.
### Оркестратор ответил на §8 (05.07) — можешь стартовать с этими ответами
- **F1-развилка: ПОДТВЕРЖДАЮ материализацию** (5eaf2d9). Это ровно **D8** (content-hash материализации, не version-counter — drift-proof: забыть бампнуть нельзя). Следуй; хеш ORDER BY-стабильных ЗАМОРОЖЕННЫХ approved-строк.
- **Граница первой вехи: ПОДТВЕРЖДАЮ AE на seeded-глоссарии** (ручной + ruby-сид); автопопуляция/адъюдикация/резюме/series-bible — отдельные вехи. **Уточнение приёмки:** порог **≥98% консистентности (D10/02-mvp) осмыслен только на ПОПУЛИРОВАННОМ глоссарии** — приёмка ПЕРВОЙ вехи = «механизм корректен на seeded-глоссарии» (инъекция/disposition/post-check/F1 работают, 0 ложных на ловушках `retrieval_bench`, resnapshot при смене approved), НЕ «≥98% на реальной книге». Это инфраструктурная веха, как Фаза 1.
- **Post-check: ПОДТВЕРЖДАЮ флаггер-до-замера** (жёсткий гейт только после валидации precision на русском). **Уточнение:** Фаза-1 post-check матчит против **записанных `decl`-форм** (Go, детерминированно; `decl` заполнен при term-commit одним дешёвым LLM-вызовом), **НЕ живой лемматизатор** (тот — Python-сайдкар Фазы 2, C3). E1-замер = precision матча `decl`-форм на реальном русском; шумит → флаг-режим, не форс.
- **Раскладка инъекции: код-собранный injection-message в `Messages()`** (вариант «а»), НЕ новый `RenderVars`-плейсхолдер: держит шаблон стабильным (`Render` падает на неизвестном `{{}}`), инъекция = динамический per-chunk контент вне фикс-шаблона, кэш-граница остаётся на стабильном system-префиксе (Р5).
- **Ruby-классификатор: детерминированное правило для ЯСНЫХ случаев** (фонетическая согласованность reading↔base / паттерн собственного имени → name-lock), **неоднозначное → флаг человеку**. Over-flag безопасно, блайнд-лок = mistranslation — зеркалит «точность > полнота» горячего пути. Не умничать: сомнение → флаг.
Welcome back to the backend. Валидируй старт, заморозь схему по реестру, построй горячий путь и инъекционную поверхность ОДИН раз, закрой F1 и post-check — и держи тихую деградацию громкой.