210 lines
17 KiB
Markdown
210 lines
17 KiB
Markdown
# Дофикс анализаторов 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 агентов умерли на лимите сессии, почти все на фазе
|
||
опровержения** — то есть находки вернулись неопровергнутыми, а не подтверждёнными. Всё, что попало в
|
||
этот отчёт, перепроверено исполнением отдельно; список неперепроверенного — раздел «найдено, не
|
||
починено».
|
||
|
||
**СТОП — приёмка оркестратора.**
|