textmachine/backend/ARCH_CLEANUP_REPORT.md

195 lines
24 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.

# 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-я реализация ПОВЕДЕНИЯ — интерфейс P1P6 = преждевременно), **без 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/<src>/` (морфология) + `<src>-<tgt>/` (Палладий); 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: строить P1P6 интерфейсы / 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-реплике (тот же сеам).