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

17 KiB
Raw Blame History

Дофикс анализаторов 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 (несёт пару LLMRequestLLMResponse, имя не важно) 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: 280ReasoningSubset подтверждён на проводе: думание внутри 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 агентов умерли на лимите сессии, почти все на фазе опровержения — то есть находки вернулись неопровергнутыми, а не подтверждёнными. Всё, что попало в этот отчёт, перепроверено исполнением отдельно; список неперепроверенного — раздел «найдено, не починено».

СТОП — приёмка оркестратора.