# ARCH_CLEANUP_REPORT — изоляция языковых данных из `pipeline/` + известные фиксы (2026-07-19) > Сессия: **Бэкенд (промежуточная)**, промт `docs/BACKEND_ARCH_CLEANUP_SESSION_PROMPT.md` (D39.15). 5 фаз-воркфлоу: > ресёрч → синтез → фикс-лист (do/defer/don't) → исполнение → ревью. **Рефактор поведенчески НЕЙТРАЛЕН для > zh→ru** (golden байт-стабилен). Не R1, не пере-прогон, не минор-хант. НЕ коммичено (лендит оркестратор). > > **Итог одной строкой:** язык-ДАННЫЕ майнера (百家姓 · инвентари · частицы · таблица Палладия) вынесены из > Go-констант `internal/pipeline/` в файлы `configs/langpacks/` через новый leaf-пакет `internal/lang` > (данные + загрузчик + контент-хеш, БЕЗ импорта pipeline); алгоритмы майнера остались в движке и читают > прокинутый `*lang.Pack` (DI, как `contrast`). Майнер офлайн → **golden байт-идентичен** (хеш не изменился); > **паритет майнера EXACT** (13618 / `{方源:0,蛊:1,蛊师:2,古月:22}` / recall 0.9649). Плюс FL-1/2/3 + тест-путь. > Живые DC-чекеры и swap `isRuTarget→x/text` — обоснованно НЕ трогаем (см. DEFER/DON'T). --- ## Фаза 1 — РЕСЁРЧ (2 направления × 3 источника, refute-by-default) Воркфлоу `arch-cleanup-research`: 8 независимых агентов (4 алгоритмы / 4 архитектура), источники — Go-stdlib/`x/text`, наш репозиторий (file:line), интернет. Ключевые находки (все грунтованы, refute-by-default): ### A. Алгоритмы - **`isRuTarget`/`isCJKTarget` → `x/text/language.Tag`: ОТВЕРГНУТО (KEEP-ASIS, HIGH-risk-если-свапнуть).** Эмпирическая проба (x/text v0.38): **6/18 реалистичных входов расходятся** в ОБЕ стороны (`ru-RU`,`rus`,`zh-Hant`, `zho`,`chi`,`ja-JP` начинают матчить; ` ru ` с пробелами ПЕРЕСТАЁт — `language.Make` не тримит). Хуже: `Tag.Base()` тянет CLDR «likely-subtags» (документированно «subject to change», зафиксированное поведение-изменение golang/go#24211) в `classifierVersion`/`cheapGateVersion` → `snapshotID` — тихая дивергенция снапшота на `go get -u` без бампа версии. `isRuTarget` ещё и гейтит cosmetic-strip санитайзера → флип меняет ЭКСПОРТ FinalText, не только флаг. Это не ДАННЫЕ, а routing-policy; сеньор оставляет явный тестируемый предикат. - **Майнер-алгоритмы = легитимный опубликованный NLP, парити-locked, НЕ велосипеды:** c-value (Frantzi/Ananiadou/Mima 2000, с задокументированным `g(L)=log₂(L+1)`), weirdness/termhood (Ahmad/Gillam/Tostevin 1999), subsumption, union-find. **Go/`x/text`-эквивалента нет** (ATE-тулкиты — Scala/Python). Из движка уходят ТОЛЬКО ДАННЫЕ-таблицы. `clusterAlias` подтверждён корректным union-find (path-halving, детерминированный вывод) → DON'T-TOUCH (директива владельца). - **Чанкер:** в `x/text` НЕТ публичного sentence-сегментатора (проверено в module-cache: нет `segment`-пакета); кастомный CJK-сплиттер намеренно ЛУЧШЕ UAX#29 (quote-depth, abbrev-guard) → менять = не-нейтрально + регресс. Фертильность УЖЕ в `config.Segmentation` (per-pair, снапшот-folded) — не трогать. `tokenClassCounts` CJK-класс = универсальное Unicode-свойство, не langpack-данные. - **Нормализация:** `memnorm.normalizeSourceKey` УЖЕ использует `x/text/unicode/norm.NFKC` + `//go:embed data/trad2simp.txt` (данные-файл с контент-хешем) — прецедент, ничего не трогаем. `pymorphy3` Go-эквивалента, стоящего порта, нет (default-B морфологию не использует). ### B. Архитектура - Проект оркестратора **ДИРЕКЦИОННО ВЕРЕН**, но переоблегчить: **0 интерфейсов сейчас** (2-я пара = 2-й потребитель ДАННЫХ, не 2-я реализация ПОВЕДЕНИЯ — интерфейс P1–P6 = преждевременно), **без auto-discovery-реестра** (конфиг-путь достаточно), тонкое зеркало **существующего промпт-сеама** (`Stage.Prompts`+`PromptPathFor`+`PromptSHA256`, резолв по `Book.LangPair()`, fail-loud). - **Внешние файлы, НЕ `go:embed`** — решает центральный пресс-тест: владелец ратифицировал «положить каталог, без перекомпиляции»; `go:embed` = compile-time. Прецедент промпт-сеама доказывает: проект принимает внешние core-данные с контент-хешем + fail-loud. - **`minerPackVersion` СЕЙЧАС НЕ фолдится в снапшот** (майнер офлайн — безвредно; A-miner/B-seam: фолдить контент-хеш, когда майнер станет живым). См. решение по фолду ниже (§ Снапшот). - **Move-vs-stay леджер (2 независимых агента СОШЛИСЬ):** двигать ТОЛЬКО офлайн-майнер-данные; живые DC-чекеры оставить (см. DEFER). Ключевая находка ревью: `translitInterjections` (ара-ара/маа/нани) — ЯПОНСКИЕ филлеры; пара-кеинг к `zh-ru` СТЁР бы детекцию ja-филлеров из ja→ru (пара самого golden). `refusalPatterns` — кросс-каттинг мультиязычная safety, НЕ двигать. Генеративити-агент (лайвнесс-рамка) пришёл к тому же независимо. --- ## Фаза 2 — СИНТЕЗ (реконсиляция ресёрч + оркестратор + владелец + фикс-лист) **Консенсус (ресёрч ⋂ оркестратор ⋂ владелец):** - Движок общий; язык-данные — файлами вне `pipeline/`; новый `internal/lang` (данные+загрузчик, БЕЗ данных-в-коде); резолв по паре; снапшот-folded как промпты; правильные алгоритмы (union-find/сортировки) не трогать. **Спорное → разрешено ресёрчем (пресс-тест сработал — отвергли 2 стартовых буллета):** 1. **`x/text/language.Tag` вместо `isRuTarget`** (буллет оркестратора И владельца) → **ОТВЕРГНУТО** эмпирикой: 6/18 расхождений + CLDR-инстабильность в снапшот-вердиктах + удар по экспорту FinalText. Держим точные предикаты. Это НЕ уклонение — фаза пресс-теста ровно для этого (промт: «ПРЕСС-ТЕСТ ресёрчем, не имплементить слепо»). 2. **Двигать живые DC-чекеры (`checkers_zh_ru` 时辰/числительные/регистр — назван владельцем)** → **DEFER**: они бегут ПАРА-АГНОСТИЧНО сегодня (регистр/интерджекции флагают ru-выход независимо от источника, `chunkrun.go:203-204`), значит пара-кеинг = ЖИВАЯ смена поведения; а фолд их хеша в `cheapGateVersion` вынуждает golden re-capture. 2 независимых агента сошлись на отсрочке к ja→ru-реплике (уже увязано D39.14). Механизм `internal/lang` они унаследуют. **Ранжирование по ценности×риску:** (1) майнер-данные — высокая ценность (百家姓/Палладий = парадигм-нарушители, названы владельцем ПЕРВЫМИ), низкий риск (офлайн, байт-нейтрально). (2) FL-1/2/3 + тест-путь — реальные, латентные. (3) DC-чекеры — средняя ценность, средний риск (живой путь, observability-only) → DEFER. --- ## Фаза 3 — ФИКС-ЛИСТ с диспозициями ### DO (сделано этой сессией) | # | Пункт | Что | Нейтральность | |---|---|---|---| | DO-1 | **`internal/lang` сеам** | leaf-пакет: `Pack` + `Load(root,src,tgt)` + контент-хеш `Version()`, БЕЗ импорта pipeline; резолв `configs/langpacks//` (морфология) + `-/` (Палладий); fail-loud на отсутствующий файл | — | | DO-2 | **Майнер-данные → файлы** | 百家姓 single/compound, title/ordinal/topo/grade/numeral/rank, alias-particle → `configs/langpacks/zh/`; Палладий (initials/finals/yw/special_i) → `configs/langpacks/zh-ru/`. Сгенерированы ИЗ живых констант (байт-точно), затем консты удалены | Майнер офлайн → golden не задет; паритет EXACT | | DO-3 | **Прокид `*lang.Pack`** | DI (как `contrast`): `MineBank/mineDetect/patternCandidates/isSurnameStart/formantType/proposeAliasEdges/isFragment/emissionEligible/isPalladiusToken/buildPalladiusCyrSyllables` читают pack. Алгоритмы В КОДЕ, данные в файлах (граница compile-enforced) | Байт-равные значения | | DO-4 | **Генеративити-пруф** | `internal/lang/langpack_test.go`: real-load zh-ru + синтетическая 2-я пара → другие байты/версия + fail-loud на неизвестную пару | — | | DO-5 | **FL-1** | Свернуть `bankTokenBudget` в `banknoteSnap.TokenBudget` (`snapshot.go`) — правка `bankMaxLines` меняет max_tokens∈request_hash без бампа parser_version = тихий re-bill; теперь громкий resnapshot механизмом | golden: banknote OFF → nil → omitempty | | DO-6 | **FL-2** | `chunkrun.go`: на резюме СОХРАНЯТЬ banknote-телеметрию (read `GetRetrievalState` при resume+enabled) вместо обнуления; ложный коммент «re-derives» исправлен на правду | golden: banknote OFF → 0 | | DO-7 | **FL-3** | `miner_emit.go`: `emitRankCap=200` кап `mr.ranked` ПЕРЕД emission-фильтрами (зеркало референса `a3[:200]`) | WHICH-инварианты на `mr.ranked` (до эмиссии) не задеты | | DO-8 | **Тест-путь** | `miner_parity_test.go`: абсолютные `/home/ubuntu/...` → env-override `TM_MINER_PARITY_{CONTRAST,RECORDS,SEED}` (skip-on-absent сохранён) | тест-гигиена, не прод | ### DEFER (в резидуал, с причиной) - **DEFER-1: живые DC-чекеры/cheapgates данные** (时辰/числительные/регистр/интерджекции). Причина: (а) бегут ПАРА-АГНОСТИЧНО сегодня → пара-кеинг = живая смена поведения (напр. ja→ru потерял бы DC6/интерджекции); (б) фолд их хеша в `cheapGateVersion` вынуждает golden re-capture, а не-фолд ослабляет drift-proofing; (в) `translitInterjections` = ja-филлеры (mis-key trap). Наследуют ТОТ ЖЕ `internal/lang`-сеам на задаче ja→ru-реплики, где ось источник/цель/пара конкретна и re-capture намеренный (увязано D39.14). Честит «известные фиксы + не над-инженерия». - **DEFER-2: `sourceAbbrevs`** (28 англ. аббревиатур в `chunker.go`) — единственный per-source-lang инвентарь в движке, но маргинален (только en-источник, вне zh/ja→ru приёмки), free-rider если появится en-пакет. - **DEFER-3: снапшот-фолд `pack.Version()`** — R1 (когда майнер станет живым). См. § Снапшот. - **DEFER-4: `isCJKTarget/isRuTarget` membership как langpack-атрибут** — только если `internal/lang`-реестр расширится под реальные корпуса; standalone = над-инженерия (A-lang F5). Резолв ОБЯЗАН остаться точным exact-match, не x/text-инференс. ### DON'T (переписывание-ради-переписывания — отвергнуто явно) - **DON'T-1: `isRuTarget`/`isCJKTarget` → `x/text/language.Tag`** — ресёрч-опровергнуто (6/18, CLDR-инстабильность, удар по FinalText). Держим точные предикаты. - **DON'T-2: двигать `refusalPatterns`** (en/ru/zh/ja) — кросс-каттинг мультиязычная safety-блэклист, disposition-ось (`classifierVersion`), модель может отказать на ЛЮБОМ языке независимо от пары. Пара-кеинг = semantically incoherent + safety-регресс. - **DON'T-3: `tokenClassCounts` CJK-класс / `yoHomographEForms` / `cjkNumeralRunes` / арифметика 万/億** — универсальные Unicode/арифметические свойства, не per-pair конвенции. - **DON'T-4: трогать алгоритмы** (union-find, детерм. сортировки, c-value, чанкер-сплиттер, регекс-ЛОГИКА чекеров). - **DON'T-5: строить P1–P6 интерфейсы / auto-discovery-реестр** — 2-я пара = данные, не поведение (нет 2-го потребителя поведения). Данные-struct + загрузчик достаточно. ### Предложения сверх скоупа (в план/владельцу) - Когда майнер станет живым (R1): фолдить `pack.Version()` в `snapshotID` (единый resnapshot тогда). - ja→ru-реплика: перенести DC-чекеры/cheapgates данные в `internal/lang` через тот же сеам (source-scoped zh + target-scoped ru + pair-scoped zh-ru), с намеренным golden re-capture — тогда ось источник/цель/пара тестируема реальной 2-й парой. - (опц.) `sourceAbbrevs` как free-rider en-пакета, если en-источник войдёт в приёмку. --- ## Фаза 4 — ИСПОЛНЕНИЕ (лог) **Порядок (низкий риск → высокий, верификация после каждого):** 1. FL-1/2/3 + тест-путь → build/vet/test зелёные, golden хеш НЕ изменился (`975fbfdc…`). 2. Данные-файлы сгенерированы **из живых констант** (одноразовый in-package генератор `TM_GEN_LANGPACK=1`, затем удалён) → гарантия байт-точности; verified `reflect.DeepEqual(loaded, const)` для ВСЕХ 13 полей до удаления констант. 3. `internal/lang` пакет + загрузчик + контент-хеш (`langpack-v1-09974d6cdac2`). 4. Прокид `*lang.Pack` через майнер; удаление констант (`surnamesSingleRaw`/`buildRuneSet`/`buildRuneSet2`/инвентари/ Палладий-мапы/`palladiusCyrSyllables`-глобал); правка тестов (helper `testLangPack`). 5. Генеративити-пруф-тест. **Верификация исполнением (мандат самопроверки CLAUDE.md):** - `go build ./...` ✅ · `go vet ./...` ✅ (чисто) · `go test ./... -race` ✅ **все пакеты зелёные** (вкл. новый `internal/lang`). - **Паритет майнера EXACT** (`TM_MINER_PARITY` присутствовали стенд-данные): `n=13618 · {方源:0,蛊:1,蛊师:2,古月:22} · recall@proposed=0.9649` — **байт-доказательство нейтральности** переноса данных. - **Golden байт-идентичен:** `sha256(capture.golden)` = `975fbfdc701438eb8604e6e1a0ddb77842246cf88983178bf99fddcab596a03e` ДО и ПОСЛЕ (не изменился — майнер офлайн, фолд не добавлен). НОЛЬ вердикт/wire/final-правок. - Диффстат: 11 файлов правлено, +157/−162 (код УМЕНЬШИЛСЯ — данные ушли в файлы); новые `configs/langpacks/` + `internal/lang/`. ### Снапшот — решение (эскалация оркестратору) Фолд `pack.Version()` в `snapshotID` **НЕ добавлен** этой сессией. Обоснование (инвариант №2 vs гардрейл промта): инвариант №2 = «фолдить то, что влияет на wire ИЛИ вердикты». Майнер **офлайн** (`MineBank` — только тест-вызовы; нет runner/tmctl-потребителя) → его данные СЕЙЧАС не влияют ни на wire, ни на вердикты. Фолд офлайн-данных вызвал бы resnapshot zh-ru-книг «за ничего» (wire идентичен) — ровно та переоплата, что дисциплина предотвращает, и ВТОРОЙ resnapshot к R1 (двойной). **Дисциплинарно-корректно: не фолдить сейчас; R1 (который делает майнер живым) фолдит `pack.Version()` тогда — единый resnapshot.** `Version()` построен и готов. Golden байт-идентичен (строго ⊆ «до одного version-поля»). A-miner/ B-seam-агенты подтвердили «фолд когда майнер станет живым». **Флаг оркестратору:** гардрейл промта в скобках называет майнер «wire-определяющим» — по факту офлайн; если оркестратор хочет фолд сейчас, это отдельное решение (golden всё равно нейтрален — ja-ru, ja-пакета нет). --- ## Фаза 5 — РЕВЬЮ (мульти-линзовый адверсариал) Воркфлоу `arch-cleanup-review`: 4 линзы (нейтральность / сеньор-код / функциональное-генеративити / FL-фиксы), author≠reviewer, refute-by-default. **Итог: линза-1 (нейтральность) — ЧИСТО (0 находок); 3 MINOR (по одной на линзы 2/3/4), 0 major/critical.** Ревью НЕ породило минор-лист — это ровно приёмочный формат. **Все 3 находки впитаны (fix + верификация исполнением):** - **R-1 (MINOR, линза-2 сеньор-код) — `parsePalladius` номер строки.** Ошибка репортила `line %d` по индексу `contentLines` (после снятия комментов) → сдвиг на число ведущих коммент-строк (сейчас 1) → правящий файл человек уходит не на ту строку. **Фикс:** `parsePalladius` сканирует сырые строки напрямую → физический номер файла. - **R-2 (MINOR, линза-3 функциональное) — fail-loud только на ОТСУТСТВИЕ файла, не на пустоту.** Док обещал «never silently empty», но пустой/коммент-только/усечённый файл давал пустую таблицу БЕЗ ошибки → на R1 (пакеты ручные) опечатка-в-пустой `surnames-single.txt` тихо гасила surname-канал майнера (регресс recall без сигнала). **Фикс:** `Pack.validate()` после загрузки — все required-таблицы non-empty, иначе fail-loud с именем пары+таблицы (делает заявленный инвариант реальным, как memnorm-паника на corrupt-таблицу). + тест `TestFailsLoudOnEmptyTable`. - **R-3 (MINOR, линза-4 FL-фиксы) — FL-2 без теста.** `TestBanknoteSliceReTelemetryResume` не пере-читал retrieval-state ПОСЛЕ резюма → тест проходил С и БЕЗ FL-2 (coverage-gap, ровно класс мандата самопроверки CLAUDE.md). **Фикс:** добавлен пост-резюм re-read + ассерт `NBanknoteLines==2`. **Верифицировано исполнением (revert-test-restore): без FL-2 тест ПАДАЕТ (`NBanknoteLines:0`), с FL-2 — зелёный.** Теперь тест реально сторожит фикс. После впитывания: `go build/vet` чисто; `go test ./... -race` **все зелёные**; golden хеш `975fbfdc…` НЕ изменился; паритет EXACT (`13618 / {方源:0,蛊:1,蛊师:2,古月:22} / recall 0.9649`). --- ## Приёмка (для оркестратора) - **Golden байт-стабильность:** хеш `975fbfdc…` НЕ изменился (даже сильнее «до одного version-поля» — НОЛЬ полей). - **Паритет:** `TM_MINER_PARITY=1 go test -run TestMinerFullBookParity` → EXACT (13618 / катастроф-скрин / recall). - **Генеративити реальна:** `go test ./internal/lang/` → 2-я пара грузится через сеам БЕЗ правок `pipeline/`; fail-loud на неизвестную пару. - **Алгоритмы не сломаны, велосипеды сняты (их не было), над-инженерии нет** (0 интерфейсов, тонкий загрузчик). - **«Не упоролись на миноры»:** DO-лист = 8 пунктов с ценностью; DON'T явно отвергнуты; 2 названных владельцем пункта (DC-чекеры, x/text-свап) обоснованно DEFER/DON'T с ресёрч-доказательством. ## Residual → R1 (`BACKEND_PLAN11_SESSION_PROMPT.md`, активен) - R1-драйвер-свитч (волны) — как было. - **Новое R1-условие:** при подключении майнера в живой W1.5 — грузить `lang.Pack` в раннере (fail-loud-at-load для пары с каталогом-пакетом; nil-and-run для пары без) + фолдить `pack.Version()` в `snapshotID` (единый resnapshot тогда). - DC-чекеры/cheapgates → `internal/lang` на ja→ru-реплике (тот же сеам).