diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 9e9b7195..6c602669 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -151,7 +151,7 @@ | 208 | **Эскалационный хоп на Gemini ТИХО ТЕРЯЕТ инъекцию банка памяти — книга едет без канона имён** (живая проба полигона 22.08, подтверждена вендор-докой и пере-проверена оркестратором по коду). OpenAI-совместимый слой Gemini принимает РОВНО ОДНО системное сообщение, лишние выбрасывает МОЛЧА — без ошибки, при `finish=stop` и внешне правильном переводе. Замер полигона на одном и том же системном тексте 7970 знаков: одним сообщением → `prompt_tokens` 2994 и маркерная инструкция ИСПОЛНЕНА, двумя → 535 токенов и инструкция НЕ исполнена. Механизм вендорский, поэтому уход на нативный API НЕ лечит: `systemInstruction` там — один объект `Content`. Наша сторона: `backend/internal/pipeline/render.go` штатно строит ВТОРОЕ системное сообщение (инъекция глоссария/STM), `backend/internal/llm/httpllm.go` `toOpenAIMessages` копирует сообщения дословно, склейки под Gemini нет нигде — а `gemini-3.1-pro-preview` стоит в ратифицированной цепочке эскалации ВО ВСЕХ четырёх арм-конфигах. ⇒ любой хоп на Gemini при непустой инъекции теряет глоссарий, и **это невидимо гейтам годности**. ⚠ Класс в проекте УЖЕ решён рядом и не обобщён: `provider_anthropic.go` сворачивает ведущий system-префикс в одно поле и падает громко на system посреди диалога. Оговорка D22.3 «экспозиция Ф1 нулевая» держится на МАРШРУТЕ (до Gemini в дефолте не доходят), а не на коде — то есть на одном включённом `budget_usd` от дефекта в проде. Полигон у себя починил склейкой по вендору (`eval/dovodka/rol.py`), в движок не лез; квирк записан в `00-provider-quirks.md` | бэкенд | **скоро (до первого платного прогона с ненулевым `budget_usd` эскалации)** | склейка системных сообщений для Gemini в транспорте + пин | пинг полигона №19 (22.08), вендор-дока `ai.google.dev/api/generate-content` | | 209 | **Риг живых проб не может залогировать тела запросов ни при каких env** (находка бэкенд-пака честности 17.08, оставшаяся без носителя до аудита доков 22.08): `LOG_LLM_BODIES=1`+`LOG_LEVEL=debug` объявлены носителем тел, но ключ читается из `obs.ReqInfo` контекста, а `live_reprobe_test.go` строит ctx БЕЗ `WithReqInfo` — гейт `obs/logging.go` не пропускает. Обход, которым пользовались: ручной дамп сообщений в файл. Цена — каждая живая проба, которой нужно прочитать провод глазами, платит за обход заново | бэкенд | когда-нибудь (следующее касание рига живых проб) | малое касание (`WithReqInfo` в риге) + пин | пак честности 17.08, вынесено аудитом доков 22.08 | | 210 | **Род НЕ ПРОИЗВОДИТСЯ авто-путём вовсе — механизм доставки построен, а данных для него нет** (найдено трассировкой цепи банка 22.08, сверено кодом оркестратором; полигон вскрыл то же независимо: «в банке НЕТ ПОЛЕЙ ПОЛА»). Доставка рода на провод ПОСТРОЕНА и работает: `membank/memory.go` кладёт `genderConstraintNote` в черновую инъекцию — но ТОЛЬКО для строк со статусом `Confirmed`, по прямой директиве владельца D39.21 «род должен доезжать». Данных ей никто не даёт: промпт терминолога возвращает РОВНО ТРИ поля (`backend/prompts/zh-ru/terminologist.md`: термин · перевод · уверенность 0–100) — поля рода в ответе НЕТ; майнер берёт `gender` исключительно из СИДА (`internal/miner/miner_emit.go` `seedMeta`, `meta.gender = e.Gender`), эмитируемые кандидаты его не несут. ⇒ **на любой книге без ручного сида ось рода мертва по построению**, и «плывущий род» — это не дефект рендера, а отсутствие входных данных. Прямо бьёт по цели №1: реестр памяти C3 называет русский глагол прош. вр. при скрытом роде «механическим спойлер-каналом» и «жалобой №1 читателей MTL» (строка 82). **Развилка, которую надо назвать до стройки:** добавлять ли поле рода в ответ терминолога (двигает провод ⇒ ТОЛЬКО в вайр-батч 182) ЛИБО выводить род отдельным дешёвым проходом, ЛИБО признать род сид-только свойством и гейтить им качество явно. Родня 82 (морфо-гейт рода) и 178 (калибровка `sense`). ⚠ **ДОПОЛНЕНО 23.08 (№19):** замка ДВА и они независимы — род рендерится только при `Disp == Confirmed` (`membank/memory.go:705-713,823-827`=`Mirrors the editor block's confirmed-only gender`), а `mining.go:676` запрещает авто-банку статус `approved`. Род НЕ единственное осиротевшее поле: `Sense`/`Speech`/`Decl` тоже приходят только из сида, авто-продюсер есть лишь у `Dst` (терминолог) и `Type` (классификатор). ⚠ Поле `Gender` в схеме сида УЖЕ ЕСТЬ (`internal/seed/seed.go:71`=`Gender string`) — ось мертва в ПИСАТЕЛЯХ, не в формате. ⚠ **И тесты слепы ПО ПОСТРОЕНИЮ** (пере-проверено 23.08): все gender-кейсы конструируют род РУКАМИ, то есть зелёная батарея не отличает «производитель работает» от «производителя нет»; гейта на дрейф рода в движке нет | бэкенд (+полигон на замер) | **скоро (цель №1; правка провода — вайр-батчем 182)** | развилка → решение → правка промпта/прохода + замер на прогоне с редактурой | трассировка цепи банка 22.08, D39.21, C3 реестра памяти | -| 211 | **⛔ Движок на SaaS НЕ ПОЛУЧАЕТ провайдерских ключей — блокер живого прогона (202).** Прод-путь `systemd-run --user` (`platform/internal/runner/runner.go:136,149`=`"systemd-run", args...`), окружение юнита = окружение systemd-менеджера + `--setenv` из `s.Env`, а платформа кладёт туда РОВНО `TM_TRACE_ID` (`runs/spawn.go:168`=`Env: engineEnv(engineStreamID(`, `engineEnv` `:268`). Замысел записан комментарием `:265-267` («движок читает ключи из .env рядом со своим book.yaml»), движок так и делает (`backend/cmd/tmctl/main.go:202`=`loadDotEnv(filepath.Join(filepath.Dir(inv.cfgPath), ".env")`; `cmd.Dir = workdir` — `runner/engine.go:176`=`cmd.Dir = workdir`, оба пути в ОДИН файл), **а кладёт туда `.env` НИКТО**: в каталоге книги платформа создаёт только `book.yaml` (`books/render.go:109`=`os.O_CREATE|os.O_EXCL`, O_EXCL) и исходник (`books/books.go:259-264`=`os.O_CREATE|os.O_EXCL`); писателя `.env` в `platform/internal/` ноль. ⚠ **Почему пережило все батареи — ловушка паритета dev/prod:** дев-супервизор запускает движок обычным процессом с `Env=nil` и наследует окружение ПЛАТФОРМЫ, что обещает его же докстринг (`ingest/supervisor.go:35-36,65-67`=`Provider keys reach the engine through it`) — стенд работает, прод голодает, гейты гоняют дев-путь. **Форма фикса (консилиум 22.08):** путь к файлу ключей аргументом у `translate` (читающим $0-глаголам ключи запрещены — `dotenv.go:53-55`), деплой-файл ПЕРВЫМ в цепочке без перекрытия, дев-супервизор на тот же механизм. Довод формы: ключи обязаны покинуть каталог пользовательского контента — сегодня бэкап/экспорт каталога книги несёт секреты. ⚠ Ключи ОБЩИЕ на всех пользователей (слово владельца 22.08); BYOK не исключён позже и ложится в ту же форму пер-книжным путём | бэкенд + платформа | **блокер строки 202** | движковый + платформенный пак | консилиум шва 22.08, пере-проверено оркестратором №19 | +| 211 | **⛔ Движок на SaaS НЕ ПОЛУЧАЕТ провайдерских ключей — блокер живого прогона (202).** Прод-путь `systemd-run --user` (`platform/internal/runner/runner.go:136,149`=`"systemd-run", args...`), окружение юнита = окружение systemd-менеджера + `--setenv` из `s.Env`, а платформа кладёт туда РОВНО `TM_TRACE_ID` (`runs/spawn.go:168`=`Env: engineEnv(engineStreamID(`, `engineEnv` `:268`). Замысел записан комментарием `:265-267` («движок читает ключи из .env рядом со своим book.yaml»), движок так и делает (`backend/cmd/tmctl/main.go:202`=`loadDotEnv(filepath.Join(filepath.Dir(inv.cfgPath), ".env")`; `cmd.Dir = workdir` — `runner/engine.go:176`=`cmd.Dir = workdir`, оба пути в ОДИН файл), **а кладёт туда `.env` НИКТО**: в каталоге книги платформа создаёт только `book.yaml` (`books/render.go:109`=`os.O_CREATE ⚠ **ДВИЖКОВАЯ ПОЛОВИНА ПОСТРОЕНА 27.08 (лендинг `d1eb8a9`, D39.158), строка об этом не знала — поймано свипом по норме §3 п.8 при ответе владельцу 27.08.** Движок принимает путь к файлу ключей аргументом (`backend/cmd/tmctl/dotenv.go:46,63`, флаг `--keys-file`, пин `cmd/tmctl/keysfile_test.go`) — то есть класть ключи в окружение юнита больше не обязательно. **Открыт ПЛАТФОРМЕННЫЙ конец:** греп по `platform/` даёт НОЛЬ упоминаний `--keys-file` — раннер его не передаёт. Значит блокер живого прогона (строка 202) снят наполовину и закрывается пунктом **(2в)** очереди D39.156, а не отдельной работой |os.O_EXCL`, O_EXCL) и исходник (`books/books.go:259-264`=`os.O_CREATE|os.O_EXCL`); писателя `.env` в `platform/internal/` ноль. ⚠ **Почему пережило все батареи — ловушка паритета dev/prod:** дев-супервизор запускает движок обычным процессом с `Env=nil` и наследует окружение ПЛАТФОРМЫ, что обещает его же докстринг (`ingest/supervisor.go:35-36,65-67`=`Provider keys reach the engine through it`) — стенд работает, прод голодает, гейты гоняют дев-путь. **Форма фикса (консилиум 22.08):** путь к файлу ключей аргументом у `translate` (читающим $0-глаголам ключи запрещены — `dotenv.go:53-55`), деплой-файл ПЕРВЫМ в цепочке без перекрытия, дев-супервизор на тот же механизм. Довод формы: ключи обязаны покинуть каталог пользовательского контента — сегодня бэкап/экспорт каталога книги несёт секреты. ⚠ Ключи ОБЩИЕ на всех пользователей (слово владельца 22.08); BYOK не исключён позже и ложится в ту же форму пер-книжным путём | бэкенд + платформа | **блокер строки 202** | движковый + платформенный пак | консилиум шва 22.08, пере-проверено оркестратором №19 | | 213 | **Платформа дублирует движковую конвенцию пути и знает суффикс чужого артефакта.** `platform/internal/runner/artifacts.go:65-95`=`neither project_db nor book_id` парсит `book.yaml` нестрогим декодером ради двух ключей и при пустом `project_db` САМА вычисляет `filepath.Join(workdir, book_id+".db")`, повторяя `backend/internal/config/book.go:189`=`b.ProjectDB = filepath.Join(dir, b.BookID+".db")`; суффикс `.bank.json` зашит на `:37`. Смена дефолта в движке тихо уводит чтение банка платформой на несуществующий путь. ⚠ Свип ВСЕЙ границы движок↔платформа (консилиум 22.08) дал ровно ОДНО такое место плюс тривиальность: дев-супервизор хардкодит `"book.yaml"` мимо константы `ConfigFile` (`ingest/supervisor.go:140`=`"status", "--config"`). Прочее знание платформы о движке — ратифицированные каналы, не гниль. **Фикс:** движок отдаёт путь артефакта аддитивно (`status --json` либо ответ нового глагола), платформа выкидывает свой `projectDB()`. ⚠ Пере-именование банк-экспорта в фикс-имя рядом с `events.jsonl` — ЛОМАЮЩЕЕ, его место в окне строки **161**, НЕ здесь | бэкенд + платформа | скоро (в те же паки) | движковый + платформенный пак | консилиум шва 22.08 | | 214 | **Подпись банка не оставляет на банке НИ ОДНОГО следа.** Единственная долговечная запись акта подписи — булев `bank_released` на прогоне (`platform/internal/pgstore/runs.go:844`=`bank_released = bank_released or $2`): кто и когда подписал, что именно вошло в подписанный набор — не хранится нигде. Следствие: пост-фактум ответить «почему в книге этот перевод термина» нельзя, а при пере-нарезке или пере-прогоне восстановить намерение человека не из чего. Всплыло трассировкой цепи банка 22.08, подтверждено пере-проверкой | платформа + бэкенд | когда-нибудь (с трубой доставки правок, D39.156) | отдельное решение о провенансе подписи | трассировка цепи банка 22.08, пере-проверено №19 | | 215 | **$0-точечный репин движка работает ТОЛЬКО под `--resnapshot`, а платформа этого флага не передаёт.** Механизм пере-привязки незатронутых юнитов при сдвиге банка построен (`backend/internal/pipeline/stagerun.go:88-107`=`POINTWISE RE-EDIT`) и он и делает подпись термина доступной по цене; но с платформы он недостижим — она зовёт `translate` без флагов сверх потолка и `--verify-bank`. ⇒ на SaaS правка термина сегодня стоила бы полной пере-редактуры, а не дельты. Родня строки 199(б) (нулевой набор consent-флагов) | платформа + бэкенд | когда-нибудь (с платформенным паком шва) | отдельное решение | трассировка цепи банка 22.08, пере-проверено №19 |