textmachine/backend/ARCH_CLEANUP_REPORT.md

24 KiB
Raw Blame History

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/isCJKTargetx/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/cheapGateVersionsnapshotID — тихая дивергенция снапшота на 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/isCJKTargetx/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-реплике (тот же сеам).