17 KiB
Дофикс анализаторов archguard + живая проба провода (бэкенд, 03.08.2026)
✅ ПРИНЯТ приёмкой оркестратора №12 (04.08, D39.97) и заленден в составе пака стандартов (семь pathspec-коммитов). Приёмка исполнением: батарея EXIT=0 · деньги вторым путём usage×цены — дельта $0.000000 по всем вызовам и сумме · две СОБСТВЕННЫЕ посадки вне карты отчёта: значение метода CAUGHT (клейм подтверждён независимо), method expression
llm.LLMClient.CompleteCAUGHT — формы нет ни в таблице, ни в списке пропусков: гард сильнее собственной документации · негативный контроль шва чист. 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_content965 символов;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(б).
- Помощник, который зовётся не
CompleteИ отмывает пару типов через свои (llm.Ask-класс). - Пара внутри named-обёртки:
typeReachesпо решению встаёт на именованном типе. - Ось видов узла: встраивание,
make([]T,1)+присваивание, generic-zero. internal/llmисключён целиком — второй провод внутри пакета адаптеров ловится только ревью.- Внешние тест-пакеты (
llm_test,config_test) исключены — следствиеinPackage, названо в коде. - Рефлексия (
reflect.Value.Call) — статическим анализом не закрывается. - Файлы за третьим сборочным тегом и в директориях, которые пропускает загрузчик. Реальная потеря охвата против байт-скана; лечится строкой в 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 агентов умерли на лимите сессии, почти все на фазе опровержения — то есть находки вернулись неопровергнутыми, а не подтверждёнными. Всё, что попало в этот отчёт, перепроверено исполнением отдельно; список неперепроверенного — раздел «найдено, не починено».
СТОП — приёмка оркестратора.