436 lines
54 KiB
Markdown
436 lines
54 KiB
Markdown
# Отчёт бэкенд-сессии: индустриальные стандарты репозитория (строки 107 · 112 · Д7)
|
||
|
||
> ⏳ **ПРИНЯТ ПО КАЧЕСТВУ КОДА приёмкой оркестратора №12 в ДВА ПАССА (03.08, D39.96); ЛЕНДИНГ ГЕЙЧЕН тремя решениями владельца (проба провода · confirm пяти mid-flight санкций, несущее — `@latest` · развилка этапа E).** Пасс-1 — шесть верификаторов по клеймам отчёта: батарея EXIT=0 (lint 0 issues, скип ровно один), 5 флипов и находка-1 подтверждены кодом (`models.yaml:191–194`, `capability.go:155–168`), голден-дифф пере-выведен независимо (только хеши; атрибуция: двигает ИСКЛЮЧИТЕЛЬНО пин 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). Носители — строки 117–123 + реестр 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()` | вынесено в `priorRenderings` — `defer` стал корректен, потому что жизнь 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.go` — `nilerr` на пропуске нечитаемого файла. Этот код удалён этапом E вместе с обходом.
|
||
|
||
**Итог:** `golangci-lint run` → **0 issues**.
|
||
|
||
---
|
||
|
||
## §3. Этап C — `make battery`
|
||
|
||
`backend/Makefile`, цели `build · vet · fmt · lint · test · battery · battery-stand · tools-check`.
|
||
|
||
- `battery` = герметичная часть: `build` → `vet` (оба тег-набора, с `-vettool`) → `gofmt` → `lint` → `test -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.git` — **Forgejo 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 build` → `cannot 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`, `pipelinePkgPath` — **396 → 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 случайных последовательностей 1–6 рун (одинаковый 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_version` → `snapshot_*` → `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.NormalizeSource` → `norm.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-lint` — **0 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) — по собственному правилу конфига их стоит включить. Носитель — эта строка.
|