Add memory-v2 onboarding prompt for the next backend session: schema freeze, deterministic hot-path injection, F1 materialization, and mandatory post-check

This commit is contained in:
Claude (backend session) 2026-07-05 03:36:28 +03:00
parent 6d96bb4ac8
commit 15cf46f7a3

View file

@ -0,0 +1,120 @@
# Промт сессии «Бэкенд — Шаг 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 флаг человеку; согласуй порог.
Welcome back to the backend. Валидируй старт, заморозь схему по реестру, построй горячий путь и инъекционную поверхность ОДИН раз, закрой F1 и post-check — и держи тихую деградацию громкой.