textmachine/docs/archive/reports/REPO_STANDARDS_2026-08-03.md

54 KiB
Raw Blame History

Отчёт бэкенд-сессии: индустриальные стандарты репозитория (строки 107 · 112 · Д7)

ПРИНЯТ ПО КАЧЕСТВУ КОДА приёмкой оркестратора №12 в ДВА ПАССА (03.08, D39.96); ЛЕНДИНГ ГЕЙЧЕН тремя решениями владельца (проба провода · confirm пяти mid-flight санкций, несущее — @latest · развилка этапа E). Пасс-1 — шесть верификаторов по клеймам отчёта: батарея EXIT=0 (lint 0 issues, скип ровно один), 5 флипов и находка-1 подтверждены кодом (models.yaml:191194, capability.go:155168), голден-дифф пере-выведен независимо (только хеши; атрибуция: двигает ИСКЛЮЧИТЕЛЬНО пин x/text, не конфиги и не x/net), 9 дефект-фиксов §2 подтверждены, 7 своих посадок в анализаторы (6/7 CAUGHT). Пасс-2 — пять АНТИ-СЛЕДОВЫХ линз (мнение до чтения отчёта · поиск вне его карты · реконструкция снесённых гардов из HEAD · аудит исключений линтера по коду · независимая репликация клеймов). Итоги пасса-2, меняющие картину отчёта: (1) ⚠ РЕГРЕССИЯ egress-паритета против снесённого байт-гарда, две исполнимые формы: пакетная llm.Complete(...) (nil-Selections → молчаливый выход, archguard.go:240-242) и интерфейс с llm-типами внутри композита (touchesPkg не рекурсивен, :79-82) — старый регекс .Complete( ловил обе; коммент «cannot be hidden» (:250-253) и клейм фикстуры фальсифицированы; снос байт-гарда в текущем виде нарушает условие промта «только после доказанного паритета» — развилка у владельца (D39.96 п.7), рекомендация: микро-дофикс ~30 строк до сноса. (2) ⚠ Находка-4(б) «порядок HPACK-заголовков изменился» — мисхарактеризация: порядок обычных заголовков рандомен per-request под ОБЕИМИ версиями x/net (map-итерация), стабильного «до/после» не существует; реальная дельта фингерпринта — только SETTINGS (MAX_FRAME_SIZE=16384 подтверждён репликацией на байтах фреймов). (3) Клейм эквивалентности x/text 0.38↔0.40 РЕПЛИЦИРОВАН независимо и шире заявленного (скаляры+битый UTF-8+пары/хангыль+энкодеры GB18030/GBK/UTF-16; три SHA-256 идентичны). (4) Посадка P3: var r pipeline.Request/new() проходят ОБА поколения гарда — «не регрессия» подтверждена ИСПОЛНЕНИЕМ на реконструкции HEAD-гарда; дыра на Request-стороне нигде не задокументирована → строка 121. (5) Аудит исключений errcheck по коду: все классы подтверждены для сегодняшнего дерева; контрпример фразе «все прод-Close — читатели» — chunktest/epub.go:51; точностные хвосты (Fprint-скоуп, w\.Write-ключ по имени, дрейф provenance-чисел 293/31/93, .skips.log не в gitignore, коммент c1 против quirks §4) → строка 122. Мелкие поправки отчёта: адреса c1 съехали на 2 (флип фактически c1:53, редакторский c1:81); счёт §4 «396→143» — фактически 395→141; строка §2 про dotenv.go устарела (финал — §9 п.14: warns, never fails); дифф go.mod несёт также indirect x/mod/x/sync. Живая проба провода НЕ добита никем (приёмке запуск заблокирован пермишен-слоем на .env) — строка 117 = гейт; довод отчёта «не было санкции» неверен (потолок $0.50 ратифицирован D39.92 п.2). Пять mid-flight решений владельца 03.08 НЕ ратифицированы — выданы владельцу на confirm/deny (D39.96 п.6). Носители — строки 117123 + реестр 108.

Промт: docs/BACKEND_REPO_STANDARDS_SESSION_PROMPT.md (D39.92). Дата: 03.08.2026. Зона: backend/ (+ этот отчёт). Правило отчёта: каждое число ниже — с командой, которой оно снято. Ничего не «зеленено» обходом.

Эхо-шапка (что понято как задача): привести репо к штатным для Go формам — линтер с конфигом, одна команда батареи, бамп зависимостей, архитектурные гарды на x/tools/go/analysis вместо самописных обходов; плюс санкционированная правка пяти шиппинг-конфигов. Ограничение владельца (стандарты не должны мешать разрабатывать и толкать модель к хакам) держится выше полноты покрытия.

Состояние: этапы 0, A, B, C, E исполнены. Этап D исполнен в части бампа и $0-верификации; живая проба провода НЕ проводилась — см. §5.


§0. Строка 112 — пять шиппинг-конфигов

Флип stages[draft].reasoning: "off" → "low" в пяти файлах:

файл строка стадия модель
pipeline-c1.yaml 55 draft deepseek-v4-flash
pipeline-c2.yaml 28 draft deepseek-v4-flash
pipeline-arm-mistral.yaml 32 draft deepseek-v4-flash
pipeline-arm-glm.yaml 36 draft deepseek-v4-flash
pipeline-arm-deepseek-pro.yaml 36 draft deepseek-v4-flash

Проверено исполнением (одноразовый пробник по LoadPipeline + Capability.applyToBody, удалён после снятия): все пять драфтов дают тело {max_tokens, reasoning_effort:low, temperature}, ThinksOnWire=true.

⚠ НАХОДКА-1: адрес pipeline-c2.yaml:39 в промте и в строке 112 ОШИБОЧЕН

Оба документа адресуют его как «черновую стадию». Это стадия select, роль judge, модель glm-5. У glm-5 capabilities.reasoning.control = extra_body_disable, то есть "off" здесь — живой выключатель, а не отсутствие ключа. Контрфакт снят тем же пробником:

glm-5  off → {max_tokens, temperature, thinking:{type:disabled}}   ThinksOnWire=false
glm-5  low → {max_tokens, temperature}                             ThinksOnWire=true

on_extra_body у glm-5 не задан, поэтому любой low|medium|high (llm/capability.go applyToBody, ветка ReasoningExtraBodyDisable) не шлёт ничего и отдаёт thinking вендор-дефолту = ON ⇒ таймауты ×3 (эмпирика полигона, pipeline-arm-glm.yaml:10). Флип дал бы ровно обратное тому, ради чего заведена строка 112. Не тронуто; в файл вписано предупреждение с причиной. Строку 112 трекера нужно поправить — это не моя зона.

Решения, принятые явно (как требовал промт)

  • pipeline-arm-mistral.yaml:44 (editor, mistral-large-2512) — оставлено "off". Модель несёт ReasoningNone и не является reasoning-моделью (models.yaml note) ⇒ ключ не шлётся, вендор-дефолт без размышления, стена «reasoning ест max_tokens» неприменима. "low" здесь = купить размышление, которого не было, без замера. Триггер пере-решения вписан в файл.
  • Редакторские off не тронуты: c1:83, c2:56, arm-glm:45, arm-deepseek-pro:45.
  • Хоп эскалации наследует ручку (stagerun.go attemptRequest берёт st.Reasoning и для hop-модели). На deepseek-v4-pro вендор маппит low→high (quirks §3а(ii)) ⇒ фактический эффорт хопа не меняется, меняется только тело запроса.
  • tmctl status --config на конфигах не гонялось — промт предписывает это, но команда принимает book.yaml и открывает БД (main.go:200 NewReadOnlyRunner), а не pipeline-конфиг. Верификация — существующие тесты, которые грузят все пять файлов: config/echo_mine_test.go, prompt_pack_test.go, content_labels_test.go.

§1. Этап A — замер ($0, ничего не чинилось)

Инструмент: golangci-lint 2.12.2 (пиновано; бинарь в ~/.local/bin, вне репо; sha256 тарбола сверен с опубликованным golangci-lint-2.12.2-checksums.txt). Дерево: 205 .go-файлов, из них 115 тестовых.

golangci-lint run --default=standard --enable=misspell --enable=nolintlint \
  --timeout=15m --max-issues-per-linter=0 --max-same-issues=0 ./...

435 находок. СТОП-порог промта (>1000) не достигнут.

линтер всего prod test
errcheck 413 124 289
staticcheck 17 5 12
misspell 4 1 3
unused 1 1 0
govet · ineffassign · nolintlint 0 0 0

govet 0 подтверждает базу «go vet чист». nolintlint 0 — потому что //nolint-директив в дереве ноль (grep -rn "//nolint" --include=*.go . | wc -l → 0).

errcheck по классам: Close 294 · fmt.Fprint* 91 · прочее 28 (8 prod / 20 test). То есть 93% errcheck — два идиоматических класса.

staticcheck по правилам: QF1001 6 · SA1024 5 · ST1018 2 · QF1003 1 · S1021 1 · S1002 1 · QF1004 1.

Дополнительные замеры (кандидаты для этапа B):

exhaustruct 988   bodyclose 0   copyloopvar 0   durationcheck 0   makezero 0
errorlint 3       nilerr 3      rowserrcheck 2  sqlclosecheck 1

⚠ НАХОДКА-2: число exhaustruct в §5.5 отчёта EFFORT_HANDLE — это СУЖЕННОЕ число, не глобальное

Глобально 988, а не 16. Суженный до двух типов, ради которых он предлагался, — 17 (include: config\.Stage, pipeline\.Request), что сходится с «16, из них 15 в тестах». Из 17 шестнадцать — тестовые литералы, а семнадцатый — САМ ШОВ (config/internal_call.go:53, InternalCall.Stage()), единственное разрешённое место постройки. Вывод промта («глобально не включать») подтверждён, но обоснование должно опираться на 988, а не на 16.


§2. Этап B — состав .golangci.yml (каждое правило обосновано числом)

Файл: backend/.golangci.yml (первый в проекте). golangci-lint config verify → чисто.

ВКЛ: errcheck 413→6 после исключений · govet 0 · ineffassign 0 · staticcheck 17→11 · unused 1 · errorlint 3 · misspell 4→0 · nilerr 3 · rowserrcheck 2 · sqlclosecheck 1 · nolintlint 0 · bodyclose/copyloopvar/durationcheck/makezero по 0 (бесплатны сегодня и не дают регрессировать; bodyclose сторожит провод к провайдерам).

ВЫКЛ с причиной (анти-хак-правило владельца: спорное правило гасится в КОНФИГЕ, а не обходом в коде):

  • exhaustruct — 988 глобально; суженный флагует сам шов, а различать шов и обход умеет только анализатор §6.
  • QF1001 (6, «применить закон де Моргана») — текущая форма условий повторяет прозу над ними; переписывание ради линтера читается хуже правила, которое кодирует. Вкусовое, не класс дефектов.
  • .Close/.Flush (294) — все 33 продовых места перечислены и проверены поштучно: это читатели и хендлы БД. Единственный в репо писатель с flush-on-Close (chunktest/epub.go, zip.Writer) свой Close проверяет, поэтому исключение не может спрятать потерянную запись.
  • fmt.Fprint* (91) — рендер CLI в stdout; сломанный пайп на tmctl status | head не то, на что CLI может отреагировать.
  • tx.Rollback (5) — идиома «откатить, если не закоммичено»; на счастливом пути ошибка = sql.ErrTxDone по построению.
  • h.Write (2) — hash.Hash.Write документирован как никогда не возвращающий ошибку.
  • initals — намеренная опечатка-фикстура: тесты лангпака пишут её в файл, чтобы доказать, что парсер ругается на неизвестный ключ. Исправление удалило бы тест.
  • misspell.locale НЕ выставлен. Замерено: locale: US даёт 309 находок вместо 4 — он срабатывает на русских комментариях.

Режим внедрения: big-bang. Критерий промта («всё с ≤20 находками») выполнен с запасом: после конфига остаётся 27 находок в 19 файлах, что ниже СТОП-порога «>30 файлов разом». --new-from-rev не понадобился.

Что из 27 оказалось настоящими дефектами, а не косметикой

место что почему это дефект
pipeline/banknote.go:303 applyBanknote мёртв все вызовы (2 прод + 5 тест) идут в applyBanknoteWithEntries; удалён, док слит с живой функцией
cmd/tmctl/backup.go:57 os.Stat → любая ошибка = «свежая книга» отказ по правам молча пропускал бэкап-гард перед платным прогоном. Стало errors.Is(err, fs.ErrNotExist)
pipeline/mining.go:384 то же на авто-банке нечитаемый файл молча ронял намайненные строки
cmd/tmctl/dotenv.go:55 os.Setenv глотал ошибку битая строка .env = ключ не загрузился, всплывало позже как 401 без причины. loadDotEnv теперь возвращает ошибку
store/voice_test.go:41,44 raw.Query с результатом в _ течёт *sql.Rows
store/glossary.go:147 три ручных rows.Close() вынесено в priorRenderingsdefer стал корректен, потому что жизнь rows теперь ограничена функцией и закрывается ДО DELETE-ов на том же tx
lang/langpack.go:338 %v вместо %w, и serr мог быть nil разделено на два случая: нечитаемо (%w) и не каталог
chunk/ingest.go:829 z.Err() != io.EOF обёрнутая ошибка прошла бы мимо
miner/miner_alias.go:161 var find func(...) + присваивание замыкание НЕ рекурсивно (итеративное сжатие путей), так что staticcheck прав

Одно место закрыто //nolint с причиной, а не правкой: synthetic_stage_seam_test.gonilerr на пропуске нечитаемого файла. Этот код удалён этапом E вместе с обходом.

Итог: golangci-lint run0 issues.


§3. Этап C — make battery

backend/Makefile, цели build · vet · fmt · lint · test · battery · battery-stand · tools-check.

  • battery = герметичная часть: buildvet (оба тег-набора, с -vettool) → gofmtlinttest -race -count=1, и в конце НАЗЫВАЕТ пропущенные тесты — молчаливый скип читается как покрытие.
  • battery-stand = то же + TM_MINER_PARITY=1 TM_CHECKER_LABELS=1, где отсутствие данных падает, а не скипается.
  • fmt проверяет ПУСТОТУ вывода gofmt -l: сама команда выходит с 0, даже когда перечисляет файлы.
  • tools-check требует ровно golangci-lint 2.12.2 и не ставит его сам: находки версионно-зависимы, а гейт, меняющийся под тобой, не гейт.

CI-воркфлоу НЕ написан — решение владельца 03.08. Установлено: удалёнка ssh://git@git.vojo.chat:2222/heaven/textmachine.gitForgejo 10.0.3 (GET /api/v1/version{"version":"10.0.3+gitea-1.22.0"}), каталогов .forgejo/ и .github/ в репо нет, раннер не подтверждён. Файл лежал бы вне зоны backend/. Точка входа до появления раннера — make battery.


§4. Этап E — гарды на x/tools/go/analysis

Новое: backend/internal/archguard/ (три анализатора) + backend/cmd/tmvet/ (multichecker). Запуск — go vet -vettool=bin/tmvet ./... из make battery, оба тег-набора.

анализатор инвариант
stageseam config.Stage строится только через config.InternalCall.Stage()
requestseam pipeline.Request собирается только в Runner.attemptRequest
egressseam к провайдеру ходит одно место — stagerun.go runAttempt (Д7)

Паритет посадками (analysistest, internal/archguard/testdata/)

# посадка чем била предшественника
1 config.Stage{…} — (исходные три дефектных сайта)
2 &config.Stage{…} байтовый скан
3 []config.Stage{{…}} элидированный тип элемента: CompositeLit.Type == nil
4 map[string]config.Stage{k:{…}} то же
5 var s config.Stage + присваивание полей литерала нет вовсе
6 new(config.Stage) то же
7 алиас импорта cfg "…/config" матч по ИМЕНИ пакета
8 дот-импорт → Stage{…} нет квалификатора
9 type myStage = config.Stage + myStage{…} алиас типа
10 type derivedStage config.Stage определённый тип поверх Stage
11 hoisted req := pipeline.Request{…} скан по RequestHash(Request{…})
12 []pipeline.Request{{…}} элидированный элемент
13 алиас импорта пакета pipeline матч по имени
14 c.Complete(…) через интерфейс
15 h.Complete(…) через конкретный адаптер

Негативные контроли в тех же фикстурах (гард, который флагует всё, бесполезен так же, как молчащий): вызов через шов InternalCall{…}.Stage() · []config.Stage как ТИП поля/параметра · пустой []config.Stage{} · pipeline.Request как параметр · чужой метод с именем Complete — байтовый скан ловил его, типовой не ловит.

go test ./internal/archguard/ -count=1   →  ok
go vet -vettool=bin/tmvet ./...          →  (пусто)

Что реально выиграно. Не «алиасы закрываются по построению» — предшественник резолвил импорты по ПУТИ и алиас уже ловил. Выиграно два конкретных свойства: (а) TypesInfo.TypeOf даёт тип элидированного литерала, поэтому []T{{…}} ловится без ручного разворачивания обёрток; (б) egress-инвариант различает НАШ Complete от чужого одноимённого метода — байтовый скан не мог и вынужден был исключать собственный исходник из совпадения с собственным же паттерном.

⚠ Слепая зона go/packages — решение принято ЯВНО

Предшественники ходили по ВСЕМУ репозиторию, доставая eval/ — Go-файлы вне модуля, где жил исторический четвёртый дефектный сайт. Анализатор так не может: ему нужен собираемый пакет.

Замерено перед отказом: в eval/ три Go-программы — softsign-decl/main.go, pkg7/termharness/main.go, pkg7/minerharness/main.go. Одна (termharness) импортирует internal/config и internal/pipeline, но ни одна не строит config.Stage или pipeline.Request, и ни одна не собирается (go.mod в eval/ нет: go buildcannot find main module). То есть охват сторожил ничто.

Решение (владелец 03.08): охват eval/ снят. Полигон — по CLAUDE.md чернорабочая зона; гейтить его не стоит ограничения на форму гарда. Если харнесс там когда-нибудь потребует покрытия, честная починка — go.mod в eval/, а не синтаксический обход здесь. Записано в доккомменте пакета.

Снесено / оставлено

Снесено (после 15/15 у анализатора, последним шагом):

  • synthetic_stage_seam_test.go: TestSyntheticStageSeamIsSingle, TestRequestIdentitySeamIsSingle, walkGoFiles, fileImports, namedType, namedTypeExact, namedTypeLocal, walkedFileFloor, configPkgPath, pipelinePkgPath396 → 143 строки;
  • content_routing_test.go: TestProviderEgressSeamIsSingle (байтовый гард Д7) — 47 строк.

Оставлено осознанно:

  • TestInternalCallDecidesEveryStageField — рефлексионный, не про обход дерева (промт прямо запрещает трогать);
  • TestBankProbeAndAttemptAddressOneCheckpoint, TestReasoningIsPartOfTheRequestIdentity, TestRepairStageTakesItsEffortFromItsOwnGate — поведенческая половина: анализатор не отвечает на вопрос, доносит ли шов ручку до денежного пути.

Пол «я реально видел файлы» (walkedFileFloor = 50) эквивалента не получил и не нуждается: go vet перечисляет пакеты сам и падает, если пакет не грузится, — обход не может тихо разобрать ноль файлов.


§5. Этап D — бамп зависимостей

go get golang.org/x/net@latest golang.org/x/text@latest gopkg.in/yaml.v3@latest modernc.org/sqlite@latest
  golang.org/x/net    v0.26.0 => v0.57.0
  golang.org/x/text   v0.38.0 => v0.40.0      ← MVS тянет принудительно
  golang.org/x/sys    v0.44.0 => v0.47.0
  modernc.org/libc    v1.73.4 => v1.74.1
  modernc.org/sqlite  v1.53.0 => v1.55.0
+ golang.org/x/tools  v0.48.0                 (под этап E)

⚠ НАХОДКА-3: бамп сдвинул денежную ось — и это был ВЫБОР, а не принуждение

ПОПРАВКА по адверсариальному ревью (03.08). Изначально здесь стояло «x/net нельзя взять отдельно — MVS тянет x/text ПРИНУДИТЕЛЬНО». Это верно только для взятого мной @latest. Замерено ревьюером на чистой копии HEAD: x/tools@v0.45.0 тянет x/net v0.54.0, чей go.mod требует x/text v0.37.0ниже нашего v0.38.0, то есть x/text НЕ двигается, и весь стек анализатора на этой комбинации собирается и проходит. Значит перекупку снапшота вызвал мой выбор @latest, а не потребность этапа E. Владелец санкционировал бамп до фронтира отдельно и осознанно (03.08, книг в работе нет ⇒ цена нулевая), но формулировка «принудительно» была неверна и снята. Индустриальный способ навсегда развязать инструментальные зависимости от денежной оси — отдельный tools/go.mod; носитель заведён в §7.

Второй ревьюер независимо доказал, что бамп не меняет поведения: NFC/NFKC байт-идентичны между x/text v0.38.0 и v0.40.0 на всех 1 112 064 скалярных значениях Unicode и на 400 000 случайных последовательностей 16 рун (одинаковый SHA-256 под обеими версиями). То есть сдвиг снапшота — чистая смена версионной метки.

x/net при этом действительно тянет x/text. А x/text вшит в memoryNormAlgoVersion (internal/text/norm.go:70) специально ради этого случая. Сработал собственный тревожный тест проекта:

xTextVersion = "v0.38.0" but go.mod pins golang.org/x/text "v0.40.0" — bump the constant IN LOCKSTEP

Константа обновлена, следом обновлены два её зеркала: text/enclosure_test.go:34 (полная строка версии) и membank/memory_f1_test.go (два хеша ComputeVersionScoped).

Заявление трекера про x/tools подтверждено исполнением. Проверено на КОПИИ реального модуля: добавление x/tools/go/analysis поднимает x/net 0.26.0 → 0.54.0. (Чистый пробный модуль этого не показывает — там x/tools приходит один; связка возникает именно на нашем графе.) То есть этапы D и E действительно сцеплены.

Голден: что именно сдвинулось

Голден пере-захвачен (TM_UPDATE_GOLDEN=1) — санкция владельца 03.08. Дифф разобран механически, не глазами:

изменённых строк: 90
изменившихся по причине, ОТЛИЧНОЙ от hex-хеша: 0

content_hash  идентичен  n=32     disp      идентичен  n=68
flag          идентичен  n=84     cost      идентичен  n=102
finish        идентичен  n=86     degraded  идентичен  n=52
tokens        идентичен  n=52     detail    идентичен  n=72
escalated     идентичен  n=68

content_hash — это msgsContentHash, подпись отрендеренных сообщений. Её неизменность означает, что байты, уходящие модели, те же. Сдвинулись только версионные идентификаторы: memory_versionsnapshot_*RequestHash (в него входит SnapshotID, render.go:309) → final_hash. Таблицы Unicode в x/text 0.40 не изменили ни одного поведения на этой фикстуре.

⚠ НАХОДКА-4 (ревью): бамп x/net РЕАЛЬНО меняет байты на проводе

Ревьюер поднял h2-слушателя с TLS+ALPN и прогнал через него дословную копию keepAliveHTTPClient (internal/llm/httpllm.go:79-99) под обеими версиями. Два воспроизводимых расхождения:

(а) SETTINGS-фрейм клиента вырос 18 → 24 байта — появился SETTINGS_MAX_FRAME_SIZE (id 0x0005) = 16384:

v0.26.0  000200000000 000400400000              000600a00000
v0.57.0  000200000000 000400400000 000500004000 000600a00000

16384 — и так протокольный дефолт, то есть семантически инертно, но это НОВАЯ байтовая последовательность.

(б) Изменился ПОРЯДОК заголовков запроса (HPACK, декодировано):

v0.26.0: :authority :method :path :scheme  authorization accept x-title content-type  content-length
v0.57.0: :authority :method :path :scheme  accept x-title content-type authorization  content-length

Набор и значения те же, регистр в обеих строчный (h2 это предписывает) — смены регистра нет, изменился только порядок.

Почему это важно. Порядок заголовков и раскладка SETTINGS — ровно те входы, по которым фронты бот-детекции (класса Akamai/Cloudflare) строят HTTP/2-фингерпринт клиента. Для провайдера за таким WAF бамп меняет наш отпечаток. Отреагирует ли конкретный провайдер — БЕЗ живого вызова не установить. Это не дефект, это неразрешённый вопрос — и ровно тот, ради которого промт требовал живую пробу.

Заодно из 78 http2-коммитов в диапазоне: «do not retry RoundTrip after peer sends a stream protocol error» · «don't PING a responsive server when resetting a stream» · «detect hung client connections by confirming stream resets» — семантика ретраев и разрыва изменилась ПОД нашим собственным retryLoop. RFC 9218 priority у нас НЕ активен: client_priority_go126.go возвращает clientPriorityDisabled()=true для !go1.27 именно чтобы не менять поведение при импорте.

⚠ Это противоречит инварианту, записанному в internal/llm/httpllm.go:75-78 («провод … не меняется, ноль байт») — он верен для полей пинга, но не для бампа, который их привёз. Строку надо поправить.

⚠ НАХОДКА-5 (ревью): вердикт-несущее использование x/text НЕ завёрнуто в версию

Бамп x/text оказался громким по случайности. Единственная сворачивающая его версия — memoryNormAlgoVersion, и она доезжает до снапшота только потому, что membank.ComputeVersionScopedIn (internal/membank/memory.go:390) пишет text.NormVersion() безусловно. Всё остальное сворачивает только stdlib-овский unicode.Version либо ничего:

Версия Сворачивает Какое x/text-поведение покрывает
checks.SanitizerVersion (sanitizer.go:119) ничего width.Fold (:687), norm.NFC (:734)
checks.CheapGateVersion (cheapgates.go:99) unicode.Version NFC-свёртку (по собственному комментарию :59)
pipeline.chunkerVersion (render.go:53) unicode.Version text.NormalizeSourcenorm.NFC (source.go:29)

detectCJKLeak сворачивает ПЕРЕД решением (sanitizer.go:211), поэтому смена width.Fold переворачивает вердикт → флаг → ремонт/эскалация → живые деньги, а SanitizerVersion стоит на месте. И сама SanitizerVersion сворачивается только при ВКЛЮЧЁННОМ гейте (snapshot.go:97-103).

Резче: ExportNormalize исполняется на КАЖДОМ отгружаемом финале, зависит от width.Fold + norm.NFC, и при этом объявлена «НЕ входом снапшота/вердикта» (sanitizer.go:709). Смена x/text там поменяла бы экспортируемые байты, не сдвинув ни одного хеша. Латентно, не сработало — но носитель нужен.

⚠ НАХОДКА-6 (ревью): xTextVersion пинит строку go.mod, а НЕ скомпилированные таблицы

golang.org/x/text/unicode/norm возит ДВА набора таблиц, выбираемых ТУЛЧЕЙНОМ: tables15.0.0.go //go:build !go1.27 и tables17.0.0.go //go:build go1.27. Сегодня go list -f '{{.GoFiles}}' подтверждает: под go1.26.5 компилируется tables15.0.0.go. Тулчейн Go 1.27 переключит x/text на таблицы Unicode 17.0.0, не тронув xTextVersion. Комментарий norm.go:62-68 утверждает, что константа делает собственную Unicode-редакцию x/text громкой — она этого не делает. Вероятно спасает смежное: stdlib-овский unicode.Version на том же тулчейне тоже сдвинется, а он свёрнут. Но это совпадение, а не механизм; стоит превратить в явную проверку.

Дифференциал нормализации: РАЗРЕШЕНО, поведение не изменилось

Исходники unicode/norm действительно правились (forminfo.go, iter.go, normalize.go: путь невалидного UTF-8 переписан с sz==0 → sz=1 на явный сентинел Properties{flags:0x80} + маску & 0x3f). Таблицы norm и весь пакет width — байт-идентичны. Прогон нормализаторов репо и всех примитивов x/text, которые дёргает internal/checks, под обеими версиями:

корпус входов результат
каждое скалярное значение Unicode, отдельно + после 5 баз + перед 6 комб. знаками + 46 фикстур 13 344 816 идентично
невалидный/усечённый UTF-8 (все 1-байтовые, все 2/3-байтовые лиды, усечения в 7 префиксах × 5 суффиксах, оверлонги, суррогаты, вне диапазона, в CJK/кириллица/хангыль-контексте) 263 560 идентично
~/books/gu-zhenren/labels/raw/corpus.jsonl — значения, строки, предложения, слова, руны, пары и тройки рун 3 561 250 входов / 12 133 355 рун / 2 568 различных кодпойнтов идентично

Ноль расходящихся кодпойнтов. Это несравнимо сильнее моего собственного доказательства по голдену.

Голден: моя проверка смотрела не на то поле

Мой масочный дифф воспроизводится, но content_hash/disp/flag/cost/... — не тот набор. Поле, способное спрятать смысловое изменение под видом хеша, это final_hash: для двух санитайзерных строк он равен sha256("tm-sanitized-v1\0" + reqHash + "\0" + stripped) (stagerun.go:355), то есть сворачивает ФАКТИЧЕСКИЙ экспортный текст. Изменившийся текст и смена версии в нём неразличимы.

Вопрос закрывает то, что голден пишет и сырой текст: final_text=%q (golden_test.go:217), stage_text=%q (:223) и 18 тел запросов (:280) — эти 68 строк байт-идентичны, проверено прямо.

Голден НЕ записывает (golden_test.go:174-176) латентность, таймстемпы, пути, URL — и, не будучи перечисленным, заголовки запроса и фрейминг транспорта. Поэтому находка-4 ему невидима by construction.

⚠ Живая проба провода НЕ проводилась

Промт требует после бампа x/net мини-вызов на КАЖДОГО провайдера (потолок $0.50) со сверкой тел, ключей, reasoning_content, finish_reason и отсутствия новых заголовков против 00-provider-quirks.md. Не сделано: отдельной санкции на трату не было, а ключи провайдеров лежат в backend/.env, который читать запрещено.

Чем закрыто вместо неё ($0): полная батарея под -race зелёная · голден разобран построчно, включая сырой текст · нормализация доказана идентичной на 17 млн входов (выше) · sqlite 1.55 проверен по changelog: новые DSN-сокращения нас не касаются, репо использует только канонические _pragma(...)/_txlock.

После находки-4 живая проба нужна СИЛЬНЕЕ, чем считал промт. httptest говорит HTTP/1.1 без ALPN, поэтому путь h2 в тестах не исполняется вовсе, а на h2 отпечаток клиента ИЗМЕНИЛСЯ (порядок заголовков + SETTINGS). Установить реакцию провайдеров можно только вызовом. Носитель обязателен.


§6. Батарея

make battery-stand   →  EXIT=0
  • golangci-lint0 issues
  • go vet + go vet -tags live, оба с -vettool=bin/tmvet — чисто
  • gofmt -l — пусто
  • go test ./... -race -count=1 — 14 пакетов ok, 0 FAIL, 0 DATA RACE

Стенд-данные на машине присутствуют, поэтому корпусные тесты реально исполнились. Замороженные числа держатся:

PARITY: n=13618 catastrophe{方源:0 蛊:1 蛊师:2 古月:22} recall@proposed=0.9655 (56/58 GT)
K6 BASELINE: tp=1 fp=14 fn=0 tn=243 disp=2
K6 AFTER (stemmer + #10): tp=1 fp=6 fn=0 tn=251 disp=2
k2 92 34/1/6/51 · k4_inverse 75 3/0/1/71 · k4a 348 0/0/0/348 · k4b 146 10/1/36/99

Артефакт контраста восстановлен. eval/exp16/data/jieba_dict_general_zh.txt отсутствовал → TestMinerFullBookParity скипался. Пере-собран из jieba==0.42.1; полный sha256 7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8 совпал с урезанным пином 7197c321…e12a8 в docs/experiments/16-bank-mining.md:183. Файл остаётся вне git (eval/.gitignore:4).

В make battery (без стенда) скип теперь ровно один — TestHelperKillLoop (подпроцессный хелпер, by design).


§7. Найдено, не починено — нужны носители

# находка почему не мной
1 Строка 112 трекера несёт ошибочный адрес c2:39 (§0) docs/ — зона оркестратора
2 Живая проба провода после бампа x/net (§5) нужна санкция на трату + ключи
3 Урезанный sha256 в 16-bank-mining.md:183 — полный записан в §6 зона оркестратора
4 jieba==0.42.1 не в eval/requirements.txt — это build-time зависимость артефакта, не runtime (ни один скрипт полигона jieba не импортирует) зона полигона
5 Число exhaustruct в §5.5 EFFORT_HANDLE читается как глобальное (§1) зона оркестратора
6 TestCheckerLabelsCandidates не содержит ни одного ассерта — не может упасть; сегодня это дизайн-REPL в одежде теста отдельная задача, не в скоупе промта
7 TestMemoryVersionPinsTheUnverifiedRenderRevision не различает «изменился рендер» и «изменилась версия нормализации» — оба двигают его литералы пин работает, но его сообщение вводит в заблуждение
8 buildCheckers жёстко берёт lang.Load(root,"zh","ru"), а бейзлайны ключуются по классу, не по паре приезд ja-ru потребует правки Go — против мандата общности
9 backend/README.md:3 заявляет, что границу импортов «проверяет go list -deps» — не автоматизировано ничем кандидат в четвёртый анализатор archguard
10 Инструментальные зависимости в главном модуле. x/tools нужен только для cmd/tmvet, но живёт в backend/go.mod и потому участвует в MVS вместе с x/net/x/text. Штатное лекарство — отдельный tools/go.mod (или tool-директива Go 1.24 в отдельном модуле): тогда линтинг физически не может двинуть снапшот требует решения по структуре модулей
11 modernc.org/sqlite 1.53.0 → 1.55.0 не требовался ничем — приехал вместе с @latest. Это драйвер под денежным леджером и чекпойнтами входит в то же решение по бампу
12 Анализатор не ловит четыре формы, дающие тот же дефект D39.87: встраивание структуры, make([]config.Stage, 1) + присваивание полей, дженерик-ноль, — предшественники их тоже не ловили (не регрессия). Класс закрывается НЕ длинным switch, а типом: сделать нулевое значение config.Stage непригодным вне своего пакета отдельная задача
13 go vet покрывает только теги default и live. Файл за любым другим build-тегом невидим (старый обход парсил исходники независимо от тегов). Сегодня других тегов в репо нет политика, не свойство

§8. Что осталось от промта неисполненным

  • Живая мини-проба провода этапа D (§5) — единственный пункт промта, не закрытый ни исполнением, ни обоснованным отказом. Нужна санкция.
  • CI-воркфлоу — не написан по решению владельца 03.08 (§3), не по недосмотру.
  • tmctl status --config на пяти конфигах — команда для этого не предназначена (§0); заменено существующими тестами загрузки.

§9. Адверсариальное ревью пака (03.08, по требованию владельца)

Четыре независимых ревьюера по диффу: корректность правок · паритет анализатора · денежная ось и детерминизм · индустриальность. Каждый проверял ИСПОЛНЕНИЕМ; двое реконструировали снесённые гарды из истории и стравили их с новым на одних подсадках.

Найдено и починено (всё — в моём коде):

# дефект статус
1 egressseam обходился локально объявленным интерфейсом с тем же методом: Obj().Pkg() становился пакетом потребителя. Байтовый гард это ловил — прямая регрессия починено сканом СИГНАТУРЫ (подставимый интерфейс обязан упоминать типы llm), посадка 17
2 Исключения потеряли сегмент /backend/ — вложенный internal/ и второй модуль platform/ наследовали их восстановлен полный путь
3 Внешние тестовые пакеты (…/config_test) не покрывались пропуском пакета ⇒ ложные срабатывания inPackage() со срезом _test
4 Конверсия config.Stage(twin{…}) не ловилась — ветка CallExpr смотрела только на new добавлена, посадка 16
5 new опознавался по ИМЕНИ идентификатора, хотя репо шадовит встроенные имена в 22 местах сверка с types.Universe.Lookup("new")
6 Инвариант ушёл из go test — держался только на make vet shippingtree_test.go: те же Analyzers() через checker.Analyze по всему модулю; доказано подсадкой
7 Пол «я реально видел файлы» не был восстановлен: пустой Analyzers() оставлял всё зелёным packageFloor + ассерт длины Analyzers() + проверка ошибок загрузки пакетов
8 go vet -vettool ЗАМЕЩАЕТ штатный набор vet, а комментарий утверждал обратное make vet гоняет оба прохода; комментарий исправлен
9 Тест-исключение errcheck из-за необязательной группы в regex глушило безымянный класс во всех 115 тестах сужено; вскрылись 12 скрытых находок — все runToSignatureStop, исключены точечно с причиной
10 $(GO)fmt — склейка строк: make GO=go1.26.5 fmt звал go1.26.5fmt gofmt напрямую
11 GO_MIN_VERSION объявлен и не использован, шапка заявляла «тулчейн пиноват» добавлена проверка версии Go в tools-check
12 Сбор скипов глушил stderr и терял свой код возврата: падение теста БЕЗ -race прошло бы незамеченным падение пробрасывается
13 loadDotEnv: os.Open глотал ЛЮБУЮ ошибку (файл под 0000 = ключи молча не загружены) различает fs.ErrNotExist, остальное — предупреждение
14 Моя же регрессия: возврат ошибки из loadDotEnv ронял status/report/export на кривой строке .env — против D20.4 («аудит-поверхность не требует ключей»), и последующие валидные строки не применялись предупреждает, не падает; остальные строки грузятся
15 Обоснование misspell.locale было фактически неверным (не русские комментарии, а en-GB↔en-US в английской прозе; 0 против 311, а не 4 против 309) переписано по замеру
16 Исключения h.Write/tx.Rollback ключевались на ИМЕНИ переменной — hasher.Write дал бы находку и толкнул к переименованию ради линтера ключ по методу
17 Числа в комментариях конфига поданы настоящим временем, хотя всё названное уже починено помечены как провенанс
18 Оборванный комментарий-хвост от снесённого гарда убран

Подтверждено чистым: analysistest честен в обе стороны (сломанный анализатор роняет тест мутацией isNamed; негативные контроли ловят лишнее срабатывание) · make battery анализатор действительно несёт (проверено подсадкой) · ложных срабатываний на []config.Stage как типе нет, и новый анализатор ЧИНИТ старое ложное срабатывание на []config.Stage{} · обе правки строковых литералов байт-эквивалентны (дубль ASCII-пробела в cutset-МНОЖЕСТВЕ; \x1f = тот же байт) · priorRenderings поведенчески идентичен, пустая карта не nil, rows закрываются до DELETE · preflightBackup/loadAutoBank — новые ошибки возникают до любой траты · miner_alias.go замыкание действительно не рекурсивно · зона соблюдена, генеративность не нарушена.

Замеры пропущенных линтеров (report-only, на этом дереве): gosec 250 (шум: G104 115 — дубль errcheck, G304 56, G306 53) · revive 80 · perfsprint 32 · exhaustive 11 · noctx 11 · prealloc 10 · gocritic 7 · errname 5 · wastedassign 2 · unconvert 1 · usestdlibvars/predeclared/nakedret 0. Ревьюер справедливо отмечает: unconvert (1) и wastedassign (2) дешевле уже включённых errorlint (3) и nilerr (3) — по собственному правилу конфига их стоит включить. Носитель — эта строка.