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

210 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Дофикс анализаторов archguard + живая проба провода (бэкенд, 03.08.2026)
> ✅ **ПРИНЯТ приёмкой оркестратора №12 (04.08, D39.97) и заленден в составе пака стандартов (семь pathspec-коммитов).** Приёмка исполнением: батарея EXIT=0 · деньги вторым путём usage×цены — дельта $0.000000 по всем вызовам и сумме · две СОБСТВЕННЫЕ посадки вне карты отчёта: значение метода CAUGHT (клейм подтверждён независимо), **method expression `llm.LLMClient.Complete` CAUGHT — формы нет ни в таблице, ни в списке пропусков: гард сильнее собственной документации** · негативный контроль шва чист. CONFIRM владельца по `@latest` принят входом. Семь осознанных пропусков → строка 121(д); нетронутый инвариант-коммент `httpllm.go:77` → строка 122. Оговорка о 20/43 умерших ревью-агентах компенсирована пере-проверкой сессии + посадками приёмки.
**Мандат:** `BACKEND_ARCHGUARD_DOFIX_SESSION_PROMPT.md` — хвост пака стандартов D39.96.
**Итог:** оба этапа закрыты. `make battery` EXIT=0, проба PASS 3/3, потрачено $0.0053 из потолка $0.50.
Не коммитил — лендит оркестратор.
## Вход приёмки
| | |
|---|---|
| Слово владельца по `@latest` (D39.96 п.6-iii) | **CONFIRM** (03.08) ⇒ проба на текущем дереве; минимальный пере-бамп не выполнялся |
| go.mod пробы | `go 1.26.4`; x/net **v0.57.0** · x/text **v0.40.0** · x/tools **v0.48.0** · yaml.v3 v3.0.1 · sqlite **v1.55.0** |
| Тулчейн | `go1.26.5 linux/amd64` |
| Пин x/text | `internal/text/norm.go:69` = `v0.40.0`, зеркала в `enclosure_test.go` и `memory_f1_test.go` |
---
## Этап 1 ($0): дыры анализаторов
Три дыры из промта закрыты, плюс три найдены и закрыты сверх него. Все шесть доказаны посадкой:
до фикса `go vet -vettool` даёт exit 0, после — exit 1.
**A. Пакетная `llm.Complete(...)`**`Selections` не заполняется для qualified identifier. Резолв
переведён на `Uses` по идентификатору вызова (`archguard.go:454`); попутно закрылся dot-import.
**B. Композиты в сигнатуре** — плоский скан разворачивал один указатель и вставал, поэтому
`Complete(ctx, []llm.LLMRequest) ([]*llm.LLMResponse, error)` проходила. Заменено на `typeReaches`
(`:156`) — рекурсия по конструкторам типа с защитой от цикла.
**C. Симметрия сеамов**`requestseam` смотрел один вид узла против четырёх у `stageseam`. Оба
переведены на общий обходчик `seamRule` (`:249`): разъезд по видам узлов и был причиной дефекта.
**D. Вызов через параметр типа (блокер, найден ревью).** `runEgressSeam` требовал `*types.Signature`;
у параметра типа `Underlying()` — интерфейс-ограничение, поэтому анализатор выходил до всякой проверки.
Реалистичная форма — Ф2-судья через дженерик — была невидима обоим драйверам. SHAPE-половина читает
теперь **место вызова** (типы аргументов + тип узла вызова), которое go/types резолвит в любом
написании; сигнатура нужна только provenance и может быть nil.
**E. Именованный результат.** `func f() (s config.Stage)` — то же объявление нулевого значения, что
`var s config.Stage`. Добавлен `*ast.FuncType`, только результаты (параметр даёт вызывающий).
**F. Исключения не были привязаны к корню модуля.** `HasSuffix` по абсолютному пути ошибался в обе
стороны: вложенная директория с тем же хвостом **наследовала** исключение, а на чекауте, где каталог
модуля назван не `backend`, целевые файлы его **теряли**. Ключ переведён на import-path + базовое имя,
позиция берётся `PositionFor(…, false)` — иначе `//line` позволяет присвоить чужую идентичность.
Сверх этого закрыта ось **имени**: оба прежних поколения гарда ключевались на слове `Complete`, так что
`llm.Do(ctx, req)` был свободен. Гард стреляет по дизъюнкции **SHAPE** (несёт пару
`LLMRequest``LLMResponse`, имя не важно) **PROVENANCE** (зовётся `Complete` и принадлежит
`internal/llm` — половина паритета с байт-сканом).
Снято ложное срабатывание: `var s *config.Stage` репортился, хотя ничего не конструирует. Разворот
указателя оставлен **только** литеральной ветви — замер показал, что для `[]*T{{…}}` он несущий.
### Форма × ловится
`✗` = проходила молча · `✓` = репортится · `—` = формы у поколения не было.
| форма | байт-гард | 1-й анализатор | сейчас |
|---|---|---|---|
| **egress** | | | |
| через интерфейс / адаптер / локальный интерфейс | ✓ | ✓ | ✓ |
| пакетная `llm.Complete` | ✓ | ✗ | ✓ |
| пара в композите `[]llm.LLMRequest` | ✓ | ✗ | ✓ |
| другое имя `llm.Do` | ✗ | ✗ | ✓ |
| значение метода `f := c.Complete` | ✗ | ✗ | ✓ |
| **вызов через параметр типа** | ✗ | ✗ | ✓ |
| запрос как параметр типа (через ограничение) | ✗ | ✗ | ✓ |
| **request** | | | |
| литерал / срез / мапа / алиас- и dot-импорт | ✓ | ✓ | ✓ |
| `var r pipeline.Request` + присваивание | ✗ | ✗ | ✓ |
| `new(pipeline.Request)` | ✗ | ✗ | ✓ |
| алиас / производный тип / конверсия | ✗ | ✗ | ✓ |
| **оба сеама** | | | |
| именованный результат `func f() (s T)` | ✗ | ✗ | ✓ |
| `var p *T`, `(*T)(x)` — ложное срабатывание | — | ✗ (репортил) | ✓ молчит |
### Мутационная самопроверка
Девять мутаций, каждая ломает один фикс; все девять пойманы, после восстановления сьюта зелёная.
| | ломает | упавшие кейсы |
|---|---|---|
| M1 | резолв через `Selections` | egress:41 |
| M2 | `typeReaches` стоп на срезе | egress:52,57 |
| M3 | `requestseam` только литерал | request:36,44,48,54,62,66 |
| M4 | SHAPE-половина off | egress:57,64,72,85,103 |
| M5 | PROVENANCE-половина off | egress:32,41 |
| M6 | egress требует сигнатуру | egress:85 |
| M7 | `typeReaches` без `TypeParam` | egress:103 |
| M8 | сеам не видит `FuncType` | stage:78, request:66 |
| M9 | литерал не разворачивает указатель | stage:98 |
Мутация нашла дефект в самом фиксе: первая редакция ветки `TypeParam` была **мёртвой**
`Constraint()` возвращает именованный тип, а обход на именованном по правилу встаёт. Исправлено на
`Constraint().Underlying()` (единственное место, где обход сознательно проходит сквозь именованный тип;
оговорка стоит в коде).
### Закрыто молчаливое гниение
Гарды ключуются на строках — переименование не роняет ни сборку, ни тест, а SHAPE-половина тихо
становится декорацией. `assertGuardVocabularyIsLive` требует, чтобы каждое исключение указывало на
существующий файл, а оба имени типов резолвились в `internal/llm`. Обе проверки проверены мутацией.
Снято мёртвое исключение `render.go`: замер показал, что дерево чисто и без него, а исключение,
не покрывающее существующего кода, молча пред-одобряет следующий литерал.
### Батарея
```
make battery # EXIT=0, 1m44s
golangci-lint # 0 issues
go vet (×2 тег-набора, с -vettool и без) # чисто
go test ./... -race # 15 пакетов ok, скип один (TestHelperKillLoop, прежний)
```
Посадок `// want`: **38** (15 stage · 13 request · 10 egress) + 5 фикстур-пропусков + 8 негативных
контролей. `TestInvariantsHoldInTheShippingTree` гоняет анализаторы по реальному модулю — **0
диагностик**, то есть ни одна из новых веток не дала ложного срабатывания на боевом дереве.
Четыре поведенческих теста живы: `BankProbeAndAttemptAddressOneCheckpoint`,
`ReasoningIsPartOfTheRequestIdentity`, `RepairStageTakesItsEffortFromItsOwnGate`
(`synthetic_stage_seam_test.go`), `InternalCallDecidesEveryStageField` (`internal_call_test.go`).
---
## Этап 2: живая проба провода
```
set -a; . ./.env; set +a
TM_LIVE=1 go test -tags live -run TestLiveAdapterConformance -v ./internal/pipeline/
```
PASS 3/3, 45s. grok закомментирован, не включал.
| модель | finish | in/out | share исходника | вердикт | цена |
|---|---|---|---|---|---|
| `deepseek-v4-flash` | `stop` | 138/380 | 0.00 | ok | $0.000126 |
| `glm-5` | `stop` | 69/21 | 0.00 | ok | $0.000136 |
| `kimi-k2.6` | `stop` | 75/1214 | 0.00 | ok | $0.004927 |
Новых finish_reason, отказов и фреймингов нет. Эхо-мина не выстрелила даже на фрагменте Лу Синя 祝福,
но это одно наблюдение: quirks:59 фиксируют, что эхо на 0731 стохастично по вызову.
### Прямой клейм по `reasoning_content`
Нейтральный `LLMResponse` полей thinking не несёт, поэтому взведён штатный лог сырых тел
(`LOG_LLM_BODIES=1` + `LOG_LEVEL=debug`, зеркало `tmctl`; `obs.LogLLMExchange` пишет только тела,
никогда URL и заголовки). Один повторный вызов deepseek, $0.000080:
- `choices[0].message` = `['content', 'reasoning_content', 'role']`, **`reasoning_content` 965 символов**;
- `reasoning_tokens: 234` при `completion_tokens: 280`**`ReasoningSubset` подтверждён на проводе**:
думание внутри completion, и наш нулевой `ReasoningTokens` — защита от двойного счёта, а не слепота;
- тело запроса — `['max_tokens','messages','model','stream','temperature']`, **`reasoning_effort`
отсутствует** ⇒ механика строки 112 подтверждена на проводе: `reasoning:"off"` на
`ReasoningNone`-модели не кладёт ничего и вызов едет на вендорском дефолте.
### h2-фингерпринт (главный вопрос D39.96 §5)
Клиент действительно ездит на x/net/http2 (`httpllm.go:17` + `http2.ConfigureTransports` поверх клона
`DefaultTransport`), и все три эндпоинта договариваются на h2 — снято сырым TLS-хендшейком за $0:
```
api.deepseek.com:443 alpn="h2" tls=0x0304
api.z.ai:443 alpn="h2" tls=0x0304
api.moonshot.ai:443 alpn="h2" tls=0x0304
```
⇒ вызовы шли с новой раскладкой SETTINGS, и ни один фронт её не отверг. Долг строки 117 закрыт.
**Граница:** три эндпоинта, по одному вызову, из одной сети, в один момент. Это «отказа нет сейчас
отсюда», не «отказа не будет». CONNECT-туннель, CONTINUATION и поведение под нагрузкой не проверены.
---
## Найдено, не починено
Осознанные пропуски по разрешённому промтом пути §D — назван в doc-комменте + посажен фикстурой без
ожидающего комментария (analysistest покраснеет в день, когда пропуск начнёт ловиться). **Приняты
оркестратором 03.08, носитель — строка 121(д), форма закрытия — типовой шов 121(б).**
1. Помощник, который зовётся не `Complete` И отмывает пару типов через свои (`llm.Ask`-класс).
2. Пара внутри named-обёртки: `typeReaches` по решению встаёт на именованном типе.
3. Ось видов узла: встраивание, `make([]T,1)`+присваивание, generic-zero.
4. `internal/llm` исключён целиком — второй провод внутри пакета адаптеров ловится только ревью.
5. Внешние тест-пакеты (`llm_test`, `config_test`) исключены — следствие `inPackage`, названо в коде.
6. Рефлексия (`reflect.Value.Call`) — статическим анализом не закрывается.
7. Файлы за третьим сборочным тегом и в директориях, которые пропускает загрузчик. Реальная потеря
охвата против байт-скана; лечится строкой в Makefile на каждый новый тег.
## Правки комментариев по факту
| было | стало |
|---|---|
| `cmd/tmvet`: «runs the invariants **alongside** the standard vet passes» | `-vettool` их **замещает** — измерено; Makefile гоняет оба прохода |
| `inPackage`: «matching the path alone let `config_test` build Stages freely» | **инверсия**: без `TrimSuffix` посаженный `config_test` репортится (exit 1), с ним молчит (exit 0) — то есть `TrimSuffix` исключение расширяет. Поведение оставлено, цена названа |
| `content_routing_test.go`: «resolving the SELECTION» | selections больше не резолвятся; переписано по фактической механике |
| `archguard.go`: «builtins carry no signature» | встроенные с неконстантным результатом сигнатуру получают (`new(T)``func(T) *T`); переписано |
| `pipeline-c1.yaml`: «echo mine unarmed» | quirks §4: `low` **пере-вооружает** эхо-мину (1 из 16); флип — осознанный размен, гейт ловит хвост |
## Оговорка о достоверности ревью
Адверсариальное ревью диффа: 6 линз, 43 агента. **20 агентов умерли на лимите сессии, почти все на фазе
опровержения** — то есть находки вернулись неопровергнутыми, а не подтверждёнными. Всё, что попало в
этот отчёт, перепроверено исполнением отдельно; список неперепроверенного — раздел «найдено, не
починено».
**СТОП — приёмка оркестратора.**