195 lines
24 KiB
Markdown
195 lines
24 KiB
Markdown
# 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/<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: строить 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-реплике (тот же сеам).
|