28 KiB
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-чекеры и swapisRuTarget→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) — не трогать.tokenClassCountsCJK-класс = универсальное Unicode-свойство, не langpack-данные. - Нормализация:
memnorm.normalizeSourceKeyУЖЕ используетx/text/unicode/norm.NFKC+//go:embed data/trad2simp.txt(данные-файл с контент-хешем) — прецедент, ничего не трогаем.pymorphy3Go-эквивалента, стоящего порта, нет (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 стартовых буллета):
x/text/language.TagвместоisRuTarget(буллет оркестратора И владельца) → ОТВЕРГНУТО эмпирикой: 6/18 расхождений + CLDR-инстабильность в снапшот-вердиктах + удар по экспорту FinalText. Держим точные предикаты. Это НЕ уклонение — фаза пресс-теста ровно для этого (промт: «ПРЕСС-ТЕСТ ресёрчем, не имплементить слепо»).- Двигать живые 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/isRuTargetmembership как 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:
tokenClassCountsCJK-класс /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 — ИСПОЛНЕНИЕ (лог)
Порядок (низкий риск → высокий, верификация после каждого):
- FL-1/2/3 + тест-путь → build/vet/test зелёные, golden хеш НЕ изменился (
975fbfdc…). - Данные-файлы сгенерированы из живых констант (одноразовый in-package генератор
TM_GEN_LANGPACK=1, затем удалён) → гарантия байт-точности; verifiedreflect.DeepEqual(loaded, const)для ВСЕХ 13 полей до удаления констант. internal/langпакет + загрузчик + контент-хеш (langpack-v1-09974d6cdac2).- Прокид
*lang.Packчерез майнер; удаление констант (surnamesSingleRaw/buildRuneSet/buildRuneSet2/инвентари/ Палладий-мапы/palladiusCyrSyllables-глобал); правка тестов (helpertestLangPack). - Генеративити-пруф-тест.
Верификация исполнением (мандат самопроверки 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-реплике (тот же сеам).
Записка оркестратору — шероховатости golden/снапшот-гарда (НАБЛЮДЕНИЕ, без правок кода в этом паке)
Провенанс: найдено в этой сессии при пресс-тесте isRuTarget→x/text и разборе, что делает golden. Это НЕ дефекты
текущего кода — сейчас всё консистентно (golden зелёный, тулчейн go1.26.4 == директива, x/text на Unicode 15.0.0, дрейфа
нет). Это слепые зоны гарда, которые всплывут при (а) следующей ратифицированной смене поведения и (б) первом бампе
тулчейна. Диспозиция — за оркестратором/владельцем. В нейтральный пак НЕ тащу: обе правки двигают снапшот/тест-инфру →
не байт-нейтральны, требуют своего ратифицированного resnapshot-захода.
Ш-1 — masked structural diff при re-capture РУЧНОЙ. TestGoldenDeterminism (golden_test.go:317) сравнивает capture
байт-в-байт; на TM_UPDATE_GOLDEN=1 просто перезаписывает файл (:306). «Маскированный дифф» (хеши/версии→плейсхолдер,
проверить что вердикты не сдвинулись) — дисциплина ревью, не код (README backend/; D39.14 делал руками). Риск: небрежный
re-capture может запечь тихую вердикт-правку под видом «version-only» — ровно то, от чего golden защищает. Опция: хелпер,
который на TM_UPDATE_GOLDEN=1 маскирует хеш/версия-поля и печатает сводку «N вердикт-изменений (final_text/disposition) /
M version-only» ДО записи → человек видит, что именно сдвинулось. Не ослабляет гард — делает re-capture безопасным по
построению.
Ш-2 — снапшот-версии не фолдят unicode.Version (мина под бамп гошки). memoryNormVersion (memnorm.go:56) хеширует
memoryNormAlgoVersion + trad2simpRaw, но НЕ Unicode-версию / версию norm-таблиц. norm.NFKC (в normalizeSourceKey —
парити майнера + матчер банка памяти) и голые unicode.Han/Cyrillic/… (~10 файлов: tokenClassCounts/cjkShare/чанкер/
санитайзер) привязаны к Unicode-версии ТУЛЧЕЙНА. Конкретный порог: x/text выбирает norm-таблицы build-тегом
(//go:build go1.27 → Unicode 15→17). Значит бамп тулчейна до go1.27 сменит NFKC/классификацию БЕЗ смены
memoryNormVersion → тихая дивергенция снапшота без сигнала (тот же класс, что x/text/language-находка §DON'T-1).
Опция: вплести unicode.Version (+ версию norm-таблиц) в memoryNormVersion и в classifier/style-версии → любой
Unicode-бамп = громкий resnapshot / красный golden, не тихий. Обязательно ДО любого go1.27-бампа.
Секвенс (предложение): обе — отдельным ратифицированным заходом, не в этом паке. Порядок при будущем бампе гошки/либ:
(1) фолд Unicode-версии → (2) авто-masked-diff в re-capture → (3) bump → (4) re-capture с пустым masked-diff +
TM_MINER_PARITY=1 re-pin. Про сам вопрос «бампать ли сейчас»: форсирующего CVE/фичи по коду НЕТ, версии свежие →
рекомендация «не бампать ради свежести» (churn + риск golden).