textmachine/docs/archive/prompts/BACKEND_SEAM_FIX_SESSION_PROMPT_2026-08-27.md

73 KiB
Raw Permalink Blame History

Промт: БЭКЕНД ДОФИКС-ПАК «ВХОДНАЯ ДВЕРЬ ШВА» (по приёмке движковой половины D39.156)

⚠⚠ ОТРАБОТАН И ЗАКРЫТ. Инструкции отсюда НЕ исполнять — файл сохранён как заказ, по которому судить исполнение.

Исход: пак входной двери шва исполнен и ПРИНЯТ 27.08 — ратификация D39.158, лендинг d1eb8a9. Дверь построена, стоп банка стал флажком, полоса отказов получила класс 15. ⚠ Якоря этого файла — на дерево ДО лендинга; часть из них лендинг сдвинул. Верить коду, не им.

Выдан оркестратором №19, 25.08.2026. Активный промт зоны бэкенда — один; этот заменяет BACKEND_SEAM_PACK_SESSION_PROMPT.md (отработан, уезжает в архив при лендинге). Параллельно законен только platform/docs/archive/PLATFORM_P8_REVIEW_SESSION_PROMPT_2026-08-22.md (ОТРАБОТАН 27.08) (read-only, пересечений нет).

1. Какая проблема и что решит твой результат

Продукт — издательский перевод больших книг мультиагентным пайплайном. Его центральная ценность — банк памяти: сквозной глоссарий имён и терминов, держащий консистентность на всю книгу.

Предыдущий пак построил входную дверь: tmctl bank-apply принимает решения владельца о банке ДАННЫМИ, а не правкой YAML руками. Пак принят не был. Приёмка прогнала цепь целиком и подтвердила, что дверь работает по назначению — но нашла один блокер и восемь мажоров, и главный из них превращает дверь в свою противоположность: один законный отказ пользователя необратимо ломает книгу так, что чинить её можно только тем самым текстовым редактором, ради отмены которого дверь и строилась.

Твой результат — дверь, которую можно отдать пользователю. Пак не переделывается: он хороший, дыры узкие и адресные. Всё ниже — правки в уже написанном коде плюс пины к местам, где гейт оказался слепым.

Дерево пака НЕ закоммичено и лежит в рабочем каталоге. Ты продолжаешь его, а не начинаешь заново.

2. Зона записи и git

Пишешь ТОЛЬКО в backend/. Читать можешь всё; platform/, frontend/, eval/, docs/ — чужие зоны, правки в них запрещены. Единственное исключение: пинги и итог сессии — в docs/PROGRESS.md, секция «Бэкенд». Ты не коммитишь — дерево готовишь и передаёшь на лендинг оркестратору (канон git-норм — CLAUDE.md). git add -A, git add ., git commit -a запрещены в любом случае. В дереве живёт незакоммиченная работа параллельных сессий — не трогай её и не «прибирай».

3. Карта чтения (пять позиций, больше не нужно)

  1. backend/docs/SEAM_PACK_FINDINGS.md — доказательная база твоего же пака. Читать §0 (состав), §5 (закрытые дефекты — чтобы не чинить дважды), §6 (класс полосы 14 — статус ратификации, см. ниже), §7 (что НЕ доказано), §9 (два остаточных риска), §10 (семь пингов). ⚠ Что из них стало заказом, точно: §9 — ВТОРОЙ риск (кап размера документа) стал §4.5; ПЕРВЫЙ (необратимость канонического пере-рендера) заказом НЕ стал и остаётся риском, он получает только пин на поле-смягчение (§4.11 б) и уточнение точности этого поля (§4.13). §10 — заказом стала ПОЛОВИНА п.3 (текст отказа, §4.14); остальные шесть перечислены в приложении «пинги, которые дофикс НЕ исполняет». Не ищи в промте заказов, которых там нет. ⚠ Статус класса 14: заводить его законно и это уже сделано — ратифицированный закон обещает, что двери встают паками, а прецедент кода 13 прошёл ровно так (направление → финализация приёмкой). Ноту и подъём обоих ратифицированных носителей полосы ставит ОРКЕСТРАТОР при лендинге; ты работаешь на классе 14 как на действующем и §4.11 (в) строишь на нём.
  2. docs/architecture/17-seam-inbound-law.md — ратифицированный закон входной двери, семь пунктов. Твой контракт. Пункты 1, 2, 3 и 7 ты трогаешь напрямую.
  3. docs/architecture/05-decisions-log.md, нота D39.156 — почему закон такой.
  4. docs/PROGRESS.md — строка БЭКЛОГА 199 ⚠ это ID строки ТАБЛИЦЫ, брать грепом ^| 199 |; буквальный sed -n '199p' даст чужой текст. В её теле — ТРИ мины формата; мины 1 и 2 пак обезвредил, мина 3 (кап реверс-секции 200 ПОСЛЕ фильтров) не обезврежена и попадает в §4.9.
  5. docs/architecture/12-go-style-notes.md — норматив общности. Ревью-вопрос по умолчанию: «заработает ли на паре, которой в репо ещё НЕТ, без правки Go?»

Код первичен. Все file:line ниже — отправные точки, снятые приёмкой на дереве пака; открывай и проверяй сам (дисклеймер о происхождении доков — шапка CLAUDE.md).

4. Состав дофикса

Каждый пункт несёт как приёмка это показала — воспроизведи сам прежде, чем чинить: правка без воспроизведённого репро лечит не то.

4.1 БЛОКЕР: reject-документ не проверяется на читаемость вообще — ДЕЛАЙ РОВНО ТАК инвариант, РЕШИ САМ форму

documentProblems (internal/membank/decisions.go) рендерит и парсит обратно только дельту. res.Rejects не участвует ни в одной проверке.

Репро (воспроизвести первым делом): решение {"action":"decline","src":"方小子","note":"\nне термин"}note начинается с перевода строки, что для свободного текста пользователя абсолютно законно. Вызов принимается: exit 0, changed=true, state: applied, rejected: []. Записанные байты:

rejects:
    - src: 方小子
      note: |4-
        не термин

seed.DecodeRejects их не читает: seed: yaml: line 1: did not find expected key. После этого любой следующий bank-apply по книге — включая --dry-run, который закон п.7 объявляет безопасной проекцией, — падает с exit 10. Тот же note на approve дверь отвергает (exit 14, ничего не записано): механизм существует и просто не применён ко второму файлу.

Заказ: результат, который дверь СОБИРАЕТСЯ записать, обязан пройти round-trip render → parse по обоим документам, и провал этой проверки — отказ, а не запись. Форму (расширить documentProblems вторым аргументом, отдельная функция, что угодно) решаешь сам и аргументируешь.

Корень глубже, и его тоже закрой: yaml.Marshal рендерит строку с ведущим \n как блочный скаляр с индикатором отступа (|4-), который его же строгий декодер не принимает. Это бьёт по ЛЮБОМУ свободному текстовому полю обоих документов, а не только по note у decline. Решение о том, нормализовать ли такой вход на входе двери или чинить рендер, — твоё, но оно обязано быть общим для всех текстовых полей, а не заплаткой на одно поле. ⚠ И прежде чем чинить: это поведение библиотеки — сходи в её доку и трекер, не гадай (гардрейл проекта на аномалии зависимостей). Если это её известный дефект — обход законен и должен нести ссылку; если наше злоупотребление — чини вызов. Формулировку «похоже, баг yaml» без сверки в комментарий не абсорбировать. ⚠ Фаззер здесь сильнее примера, и его слепота СТРУКТУРНА — вот она. Оракул FuzzSeedDocumentRoundTrip («движок не должен рендерить документ, который сам не читает») исправен: приёмка досеяла один вход с ведущим \n в note, и он падает мгновенно. Не сработал он потому, что фаззится СЫРОЙ YAML — то есть корпус по построению состоит из строк, УЖЕ переживших YAML-разбор, а настоящий вход двери это JSON-строка, на которую YAML не накладывает никаких ограничений. Ни один из семи сидов не несёт многострочного значения вовсе. Заказ: фаззить путь ДВЕРИ — произвольные строки полей Decision через ApplyDecisions в рендер и обратно, — и оракул «что движок ЗАПИСЫВАЕТ, движок ЧИТАЕТ ОБРАТНО» распространить на ОБА документа.

4.2 Пред-существующие поломки нельзя чинить по одной — ДЕЛАЙ РОВНО ТАК

Гейт «отказывают только НОВЫЕ проблемы» сравнивает тексты сообщений (known[p]), а вердикт загружаемости приходит ОДНОЙ строкой, несущей ВСЕ битые термы сразу: ParseBankSeed копит problems по каждому терму и склеивает их в одну ошибку, а documentProblems сворачивает эту ошибку в один элемент []string. Поэтому починка одного терма меняет строку ЦЕЛИКОМ, совпадения с known больше нет, и вызов отказывает.

Это НЕ «загрузчик падает на первой строке» — прежняя редакция промта утверждала так и была неверна (испр. по опровергателю промта). Разница несущая: чинить надо разложение многосубъектного вердикта на проблемы ПО СУБЪЕКТАМ, а не накопление ошибок в общем загрузчике сида — он уже накапливает, и он читает glossary_seed КАЖДОЙ книги.

Репро: дельта с двумя строками status: approved, dst: "". Решение чинит одну → exit 14, и текст отказа называет ВТОРУЮ строку, которой решение не касалось. Обе одним документом → exit 0.

Это прямо опровергает собственный контракт двери (комментарий поля Preexisting: «a book whose files already contradict each other has to stay repairable through this door»).

Заказ: пред-существующая проблема опознаётся по СУБЪЕКТУ (терм/поверхность, которой она касается), а не по тексту сообщения. Проверено приёмкой: несвязанное решение при одной пред-существующей поломке проходит корректно, ломается именно инкрементальная починка НЕСКОЛЬКИХ.

4.3 Два ключа на один путь теряют решение молча — ДЕЛАЙ РОВНО ТАК

Репро (воспроизведено приёмкой): book.yaml, объявляющий mined_delta и mined_rejects на ОДИН путь, файл при этом пустой — то есть парсится обеими схемами. Результат: exit 0, оба решения applied, а на диске остаётся только reject-лист. Одобрение владельца исчезает молча, и файл после этого не грузится как дельта — следующий прогон падает на разборе конфига.

Заказ: config.LoadBook отказывает, если два пути совпадают. Одна проверка плюс строка отказа.

4.4 Гард флага ключён на ЗНАЧЕНИЕ, а не на присутствие — ДЕЛАЙ РОВНО ТАК

invocation.go: if *decisions != "" && cmd != "bank-apply". Поэтому --decisions= с пустым значением гарда не задевает.

Репро: tmctl translate --config b.yaml --decisions= идёт по ПЛАТНОМУ пути ровно как без флага (проверено: обе формы падают на одной и той же поздней стадии). Реальный триггер — --decisions=$DOC с незаданной переменной в юните или скрипте. Это ровно класс R2 твоего же §5, который ты назвал «стоил бы ДЕНЕГ», уцелевший в одной форме.

Заказ: оба гарда (--decisions, --keys-file) ключить на ПРИСУТСТВИЕ флага, не на непустоту значения. Пустое значение у присутствующего флага — тоже отказ. ⚠ Присутствие даёт только fs.Visit — он обходит ТОЛЬКО реально переданные флаги; fs.Lookup на этот вопрос не отвечает вовсе, он возвращает ОБЪЯВЛЕННЫЙ флаг независимо от того, передавали его или нет. Живой прецедент лежит в том же файле: ceilingGiven через fs.Visit, с комментарием, объясняющим ровно эту разницу. ⚠ Комментарий над самими гардами УЖЕ говорит «scoped to the flag being PRESENT» — то есть код расходится со своим же комментарием, и это дополнительная улика.

4.5 Документ решений не капирован, а OOM движка платформа читает как успех — ДЕЛАЙ РОВНО ТАК инвариант, РЕШИ САМ число

os.ReadFile(decisionsPath) без ограничения. Замерено приёмкой на документах из одних approve: 20 000 решений → 0.730.80 ГБ RSS, 40 000 → 1.42 ГБ при документе всего в 2 МБ, то есть ~36 КБ памяти на решение. Разброс на 20 000 — два прогона документов слегка разной формы (с note и без), а не две разные оси.

И вторая половина, которой в §9 не было: движок при нехватке памяти выходит кодом 2 (fatal error: ... out of memory — Go так завершается), а платформа читает 2 как CompletedWithFlags — «команда СДЕЛАЛА свою работу, часть юнитов на человека» (platform/internal/ingest/exit.go, читать, не править). То есть смерть движка от памяти прочитывается как успешный прогон. Класс пред-существующий и общий для всех глаголов — но bank-apply первый, чей размер входа задаёт ПОЛЬЗОВАТЕЛЬ.

Заказ: кап на размер документа решений плюс внятная строка отказа полосой. Число выбираешь сам и аргументируешь; ориентир — платформа капирует свои чтения от движка (16 МБ статус / 256 МБ банк). Хвост про exit 2 — пингом, не тихой правкой: он общедвижковый и не твой скоуп.

4.6 artifacts{} не доезжает до своего потребителя — РЕШИ САМ форму

Проверено приёмкой грепом: Artifacts существует только в internal/pipeline/status.go, а единственный код платформы, выводящий движковый путь (runner.ReadBankprojectDBfilepath.Join(workdir, book_id+".db") + суффикс .bank.json), зовётся из её read-model, чей интерфейс движка — {Manifest, Export}, без Status (platform/internal/readmodel/, читать, не править).

Буква §4.5 прежнего заказа исполнена, цель строки бэклога 213 — нет: платформа не может выкинуть свой projectDB(), не заведя третий вызов движка на каждую материализацию.

Заказ: тот же версионированный конверт путей отдать на поверхности, которую потребитель уже читает. manifest --json — самый дешёвый кандидат (read-model уже его гоняет), но форму и состав выбираешь ты и аргументируешь. ⚠ Пере-именование банк-экспорта в фикс-имя по-прежнему ЛОМАЮЩЕЕ, его окно — строка 161, сюда не тащить.

Твой же пинг §10 п.5 («опубликованный путь читается как приглашение его парсить; единственная изгородь — комментарий») этим углубляется, и это осознанно: закон п.1 разрешает платформе читать по путям, СООБЩЁННЫМ движком, а п.3 требует версионированного конверта на каждом выходе. Изгородью становится конверт, а не комментарий. Если считаешь, что этого мало, — скажи аргументом, это законный ответ по §11.

4.7 Отказ по алиасу терма, одобренного в ТОМ ЖЕ вызове, принимается инертным — ДЕЛАЙ РОВНО ТАК

Проверка aliasOwner смотрит в res.Delta ДО цикла применения, поэтому терм, одобряемый решением i, ещё не лежит в дельте, когда судится решение j.

Репро: один документ = approve 方源 + decline 方小子, где 方小子 — алиас кластера майнера у 方源. Оба отчитываются applied, rejected: []. В дельте 方小子 остаётся ФАЙРЯЩИМ алиасом одобренного терма, а reject — только фильтр предложений. Прозвище продолжает подставляться в переводе, владелец уверен, что снял его. Те же два решения ДВУМЯ вызовами — корректно отвергаются по имени.

Заказ: правило судит НАБОР, как и заявлено в шапке ApplyDecisions, а не только состояние файла до вызова.

4.8 Перевёрнутое окно глав принимается — ДЕЛАЙ РОВНО ТАК

resolveDecision проверяет только < 0. Воспроизведено приёмкой: решение с since_chapter: 90, until_chapter: 3 принимается и записывает в дельту строку, которая не сработает ни в одной главе. Отказывать.

4.9 Мина 3 строки 199: «применено» ≠ «стоп снимется» — РЕШИ САМ форму

reverseSectionTerms режет кап 200 ПОСЛЕ фильтров, поэтому на книге с >200 banknote-only поверхностями подпись всей карты стоп НЕ снимет: приедет свежая двухсотка, и актов подписи понадобится ceil(N/200).

Механизм против этого паком не заказан и сейчас не заказывается. Заказывается ЧЕСТНОСТЬ отчёта: вызов, после которого стоп заведомо не погаснет, не должен читаться как «готово». Форму (поле в отчёте, строка предупреждения, что-то ещё) решаешь сам и аргументируешь; если считаешь, что дверь этого знать не может — скажи это аргументом, и это будет принято.

4.10 SIGTERM игнорируется, лок держится — РЕШИ САМ форму

Проверено приёмкой: на длинном документе процесс переживает SIGTERM и продолжает держать эксклюзивный лок проекта; снимается только SIGKILL. Платформа останавливает движок именно SIGTERM (platform/internal/runner/, читать, не править), а лок блокирует прогон книги.

Закон п.2 отводит SIGTERM роль «дочти и отпусти» для ПРОГОНА; для $0-глагола аналог — «брось работу и отпусти лок, ничего не записав»: у всё-или-ничего нет частичного результата, который стоило бы дописывать. Форму решаешь сам.

4.11 Восемь слепых пятен гейта, десять мутаций — ДЕЛАЙ РОВНО ТАК

Каждое ниже — место, где приёмка посадила мутацию и батарея осталась зелёной. Живого дефекта в них нет; отсутствует пин. ⚠ Считай по МУТАЦИЯМ, а не по строкам таблицы: строка (д) несёт три независимые посадки, и пин нужен на каждую. Каждый пин обязан краснеть на своей мутации — покажи это в отчёте прогоном.

что не сторожится мутация, которую пин обязан поймать
а арбитраж лока против ПРОГОНА. TestBankApplyRefusesABusyProject держит лок тем же store.LockProject, поэтому проверяет «LockProject конфликтует с LockProject». Инвариант ратифицирован (закон п.2, «тот же flock проекта») сменить файл лока в LockProject (dbPath + ".lock" → любой другой) — сегодня батарея зелёная. Пин обязан держать лок так, как его держит ПРОГОН (store.Open)
б canonical_rewrite и preexisting_problems — два опубликованных поля отчёта, на которых пак держит смягчение двух СВОИХ ЖЕ названных рисков (§9 необратимый пере-рендер, R7 проглоченная поломка) занулить оба (rep.CanonicalRewrite = false, PreexistingProblems: []string{}) — сегодня батарея зелёная
в гейт тотальности таблицы отказов слеп к сценарию, который сам называет. TestTheRefusalTableIsTotal держит РУЧНОЙ список классов и сверяет его длину с длиной refusalExit добавить шестой класс в pipeline.RefusalClass, не внося его НИ в refusalExit, НИ в список теста — сегодня тест ПРОХОДИТ, а класс уехал бы в catch-all 19. Перечисляй классы из ИСХОДНИКА. ⚠ TestDispatchCommandsCoversTheSwitch — прецедент ТЕХНИКИ, но не реализации: он ключён на *ast.BasicLit и потому слеп к case с константой (это его собственный пинг, §10 п.2 отчёта), а значения RefusalClass объявлены именно КОНСТАНТАМИ — копирование один-в-один воспроизведёт слепоту. Нужен обход GenDecl/ValueSpec
г нормализация в REPLACEMENT-половине отказа. Тесты используют поверхности, на которых NormalizeSourceKey — тождество nk := text.NormalizeSourceKey(src)nk := src в dropTerms. Пин: одобрен 虫, отказ приходит по 蟲 (традиционная форма) — отказ обязан снять одобрение
д глубокое копирование алиасов в withRubyAliases — ровно класс R4/R5, ради которого поле Ruby заведено; перенос note при промоушене; рантайм-половина гарда ОБЪЯВЛЕННОГО пути (decisionFilePresent(path, declared=true)) снять глубокую копию · if false { t.Note = d.Note } · case !declared && ...case ... — все три сегодня зелены
е абсолютность путей в artifacts{} запинена ВАКУУМНО. Проверка filepath.IsAbs в statusartifacts_test.go есть, но фикстура даёт уже абсолютный --config, поэтому absPath в ней ничего не делает и его удаление не краснит. Замерено: с ОТНОСИТЕЛЬНЫМ --config absPath — несущий, и без него потребитель в другом рабочем каталоге прочитает не тот файл убрать absPath у любого из четырёх путей — сегодня батарея зелёная. Пин обязан звать глагол с относительным --config
ж байтовый гейт changedDoc не запинен: мутация deltaChanged := res.DeltaTouched (байтовый гейт снят целиком) оставляет и TestBankApplyRepeatIsAByteNoOp, и TestBankApplyWritesTheConventionalFiles зелёными. Контракт «байтовый no-op» держится сегодня только на *Touched из чистого слоя — второй рубеж не защищён снять байтовое сравнение в changedDoc
з правило «файлы решений едут С КНИГОЙ, а не с project_db» не запинено: конвенционный дефолт можно перевесить на каталог project_db, и батарея зелёная — во всех тестах два каталога совпадают. Правило несущее: project_db может указывать куда угодно, а решения владельца обязаны ехать с книгой в бэкапе и экспорте каталога перевесить дефолт на filepath.Dir(b.ProjectDB); пин обязан развести два каталога

⚠ Уже существующие пины, которые тоже придётся починить, вынесены отдельным номером — §4.15.

4.12 Цепь §8 — ЗАКРЫТА ПРИЁМКОЙ, забери её в репозиторий — ДЕЛАЙ РОВНО ТАК

Твой §8 просил приёмку прогнать цепь целиком и объяснял, что своими силами это невозможно: «до стопа надо дойти ОПЛАЧЕННОЙ черновой волной». Довод оказался шире правды. В репозитории уже есть харнесс, гоняющий тот же банк-стоп против ФЕЙКОВОГО провайдера за $0 — internal/pipeline/miningstop_join_test.go: setupMiningStopProject + newJSONProvider + newVerifyRunner + runToSignatureStop + readSignatureMap, на них стоит ~30 живых тестов.

Приёмка написала и прогнала цепь целиком, и она работает: стоп на 2 термах → bank-apply промотит dst-несущий и отклоняет WHICH-only → резюм → «empty delta, auto-continuing to the edit wave», редакторская волна отработала. Мина 1 строки 199 (ливлок на status: auto) не сработала.

Заказ: этот тест обязан жить в репозитории, потому что он — единственный, кто видит ливлок уровня ЦЕПИ, которого частичные тесты по построению не видят. Собери его сам по описанию выше: дойти до стопа → разобрать карту подписи seed.DecodeFile → построить документ, где термы с dst идут в approve, а WHICH-only в declineApplyBankDecisions → пере-запустить прогон → убедиться, что стоп ПОГАС и редакторская волна пошла.

Как понять, что собрал верно. На фикстуре без опций стоп приходит на ДВУХ термах — один с dst, один WHICH-only, — то есть документ решений выходит «1 approve + 1 decline», bank-apply отвечает changed=true, accepted=2, а файлы у него КОНВЕНЦИОННЫЕ (ключей фикстура не объявляет). На резюме движок пишет в журнал skipped_as_declined=1 emitted_by_miner=0 и «empty delta, auto-continuing to the edit wave», после чего редакторская волна отрабатывает. Если у тебя иначе — собрано не то.

Ограничение назови в комментарии теста сам: синтетический контраст меняет, КАКИЕ кандидаты предложены, но не то, проходит ли подписанная строка фильтр подписи. Для вопроса «гаснет ли стоп» это корректно; для замера качества майнера — нет.

⚠ Соседний факт, поймай его при написании: TestMiningStopRejectClearsDelta заканчивается проверкой «погасший стоп не оставляет карту подписи», но использует ВТОРОЙ, свежий проект, где карта никогда не писалась, — проверка почти вакуумна. На ОДНОЙ книге карта остаётся на диске: движок её нигде не удаляет (os.Remove в mining.go не встречается). Поведение пред-существующее, паком не введено — пинг, не правка, но пусть твой тест это зафиксирует явно.

4.13 Мелочи — РЕШИ САМ, чинить или отклонить с аргументом

  • canonical_rewrite — ИЛИ по двум файлам, поэтому предупреждает про файл, который вызов не откроет (замерено: на книге, чьи оба файла записал движок, флаг корректно false; горит, когда комментарии оператора несёт reject-лист, а решение трогает только дельту). Потребитель рисует предупреждение не про то.
  • note нельзя ОЧИСТИТЬ: повторное решение без note сохраняет прежнюю заметку. Заказом не оговорено ни в ту, ни в другую сторону — назови поведение решением, каким бы оно ни было.
  • book_id не проверяется на путевую безопасность, а путь строится ДО валидации: book_id: ../x уводит оба файла решений выше каталога книги. Класс пред-существующий (<book_id>.db), пак расширил его с одного файла до трёх. На SaaS book_id генерируется платформой, то есть пользователю недостижим.
  • Каталог книги без права записи → exit 1, ВНЕ полосы отказов (store: ... attempt to write a readonly database). Платформа спишет проблему прав ХОСТА в бюджет попыток КНИГИ. Класс пред-существующий (store.Open ведёт себя так же), новый глагол его унаследовал.
  • Проекция создаёт <project_db>.lock в каталоге книги. Требование §4.2 «проекция БЕЗ мутации» держится (файлы решений не тронуты), но «не трогает ничего» буквально неверно.
  • Порядок двух записей пессимален для decline. Комментарий обосновывает «дельта первой» тем, что крах между записями оставит промоушен без снятого отказа — верно для approve. Для decline тот же порядок теряет решение ЦЕЛИКОМ: дельта переписана (одобрение снято), reject не записан. ⚠ Приёмка это НЕ воспроизвела — атомарная замена обходит права на файл, а настоящее окно микросекундное; повтор вызова при этом сходится. Утверждение остаётся рассуждением, не замером: реши сам, стоит ли оно правки, и аргументируй.
  • artifacts.bank_export печатается безусловно, в том числе для книги, где файла ещё нет (проверено на копии стенда). Читатель платформы это переживает (обычная ошибка открытия), но поле именует МЕСТО, а не наличие — реши, достаточно ли этого, и назови решение.
  • aliasOwner возвращается на ПЕРВОМ совпадении, поэтому исход decline в теории зависит от порядка строк дельты, если поверхность одновременно алиас одной строки и src другой. ⚠ Такой документ уже ловится ApprovedSharedKeyCollisions, и приёмка значимого расхождения воспроизвести не смогла — это гипотеза, а не находка. Отклонить с аргументом законно.
  • Проекция отвечает «что применится», а не «почём». Закон п.7 звучит как «почём ДО подтверждения», прежний заказ денег в проекции не требовал, а смета уже едет в status --json. Это вопрос ДИСПОЗИЦИИ, а не дефект исполнения: не чини, а вынеси пингом со своей рекомендацией — решать оркестратору.

4.14 Довод отказа --keys-file не покрывает всех, кого отвергает — ДЕЛАЙ РОВНО ТАК

Текст отказа (cmd/tmctl/invocation.go) звучит так: «--keys-file принимает только translate, не %q: $0-читающие команды (report/status/export/manifest/seed-lint) не должны требовать ключей вовсе (D20.4)». В ту же ветку попадают redrive (платный путь: «only a real redrive re-attacks and re-bills»), backup и migrate (деплой-шаги). Довод их не описывает, и оператор, получивший этот отказ на redrive, читает про себя неправду.

Заказ: довод обязан покрывать всех, кого ветка отвергает. Одна строка. ⚠ Саму развилку «нужен ли redrive канал ключей аргументом» НЕ решай — прежний промт сказал «флаг только у translate», и это исполнено верно; развилка остаётся пингом.

4.15 Три уже существующих пина тавтологичны или узки — ДЕЛАЙ РОВНО ТАК

Вынесено отдельным номером, чтобы механическая сверка §7 их видела.

  • Вердикт Preexisting исполняется одной проверкой из четырёх: TestAPreExistingCollisionDoesNotBlockAnUnrelatedDecision гоняет только ApprovedSharedKeyCollisions; MinedDeltaSeedCollisions, GenderVocabViolations и UnknownVoiceCharacters не исполнены ни одним тестом.
  • Четвёртый оракул FuzzDecisionDocument пере-выполняет ровно то, что documentProblems уже выполнила внутри ApplyDecisions, и заперт под len(res.Rejected) == 0 — тавтология.
  • TestDecisionByBankID вычисляет ожидаемый id той же функцией, которую проверяет, поэтому не видит потери инъективности — свойства, ради которого в TermID и сделана преамбула про length-prefix. Сверяйся с независимо посчитанным значением, а не с самой функцией.

4.16 Деньги: платных вызовов в паке НОЛЬ — ДЕЛАЙ РОВНО ТАК

Санкции на платные вызовы нет и не запрашивается. Всё выше проверяется юнит-тестами, фейковым провайдером и пробой на копии книги стенда. Упёрся в место, где без денег не доказать, — пинг с названной суммой, не трата. Рантайм-вердикт по платному пути остаётся PLAUSIBLE — это ожидаемый исход, а не дефект отчёта; так и напиши.

5. Мандат самопроверки ИСПОЛНЕНИЕМ

Не «перечитал» — исполнил. Минимум:

  • Батарея целиком, ИЗ КАТАЛОГА backend/, после каждой содержательной правки: go build ./..., go vet ./..., go test ./... -count=1, make lint. ⚠ go.mod в корне репозитория НЕТ — модули раздельные. ⚠ И make battery-stand — обязательно, минимум один раз в конце. Пак владеет internal/standdata (тест-резолв корпуса), а регрессию в резолвере обычная батарея не ловит по построению: без корпуса эти тесты просто скипаются. Ожидаемая картина — всё зелено, кроме TestMinerFullBookParity, красного из-за отсутствующего jieba-контраста (он лежит по внутрирепозиторному пути, но вне git по построению — клон его не получает; это строка бэклога 123, не твой дефект). Красный тест не «подгоняется»: править или удалять тест, голден или гейт ради зелени — НЕДОПУСТИМО; несогласие с тестом — пинг оркестратору, а не правка. ⚠ В internal/store живут два пред-существующих -race-флейкаTestTheSeamIsNotChargedToTheStoreOperationBudget и TestKillMinus9LosesAtMostOneCall. Приёмка доказала тремя независимыми путями, что они не от пака: правка store.go чисто аддитивна, покрытие новых функций под обоими тестами ноль, и чередующийся A/B против дерева без пака даёт ту же частоту. Оба меряют ВРЕМЯ и чувствительны к нагрузке. Красное ИМЕННО в них — шум; красное в любом другом тесте — регрессия.
  • Собственные адверсариальные посадки ВНЕ списка §4.11. Десять названных там — обязательный минимум, а не потолок: посади свои в новый код и покажи, что батарея краснеет. ⚠ Посадка обязана называть ПАКЕТ, в котором ищется пин — не тот, где лежит правка: ложное «мутация выжила» опаснее пропущенной, потому что выглядит как результат. ⚠ Харнесс мутаций обязан снимать отпечаток целей до и после — твой же §11 п.1 объясняет цену.
  • Живая проба на копии книги стенда (<репозиторий>/books/gu-zhenren/*). Работать в КОПИИ вне рабочего дерева; правки в books/ не делать вовсе — это отдельный git-репозиторий и не твоя зона.
  • Субагенты РАЗРЕШЕНЫ явно — на ревью своего кода, на поиск дефектов вне твоей карты, на опровержение твоих же выводов. Дефолт-запрет харнесса иначе тихо победит. ⚠ Соразмеряй параллельность с ресурсами: субагенты, одновременно гоняющие полную батарею, топят друг друга, и упавший от нагрузки агент выглядит как отсутствие находок.
  • Артефакт-находки обязателен: дописывай backend/docs/SEAM_PACK_FINDINGS.md новой секцией (не переписывай старые — они улика приёмки), что посадил, что краснело, что выжило.

5.1 Обязательное для кодового пака

  • Последний абзац отчёта — это план или обещание? Значит сделай его СЕЙЧАС.
  • Дифф ^func Test — исполнением, а не по памяти: покажи командой, какие тесты добавлены и тронуты.
  • Интервальная самоверификация субагентом против ЯВНЫХ критериев в середине работы.
  • Перед отчётом сверь каждый клейм с результатом инструмента ЭТОЙ сессии.

6. Оси ревью (три, по характеру работы)

  1. Round-trip формата — главная ось этого дофикса: всё, что дверь ЗАПИСЫВАЕТ, она обязана ПРОЧИТАТЬ обратно, на обоих документах и на всех текстовых полях. Фаззер здесь сильнее примера.
  2. Слепота гейта — для каждой правки: какой мутацией она сторожится и краснеет ли пин НА САМОМ ДЕЛЕ. Восемь мест §4.11 — это там, где гейт уже оказался слепым; не заведи девятое.
  3. Общность — заработает ли на паре, которой в репо нет, без правки Go. Приёмка проверила дверь на en→de и ja→en и нашла её чистой; твои правки не должны это сломать.

Ты вправе ЗАМЕНИТЬ любую ось своей, если аргументируешь, чем твоя лучше ловит дефекты этой работы.

7. Записка-план

До кода — короткая записка backend/docs/SEAM_FIX_PLAN.md: что делаешь, в каком порядке, какие развилки видишь. Комплектность против §4 сверяется МЕХАНИЧЕСКИ (таблицей или грепом), а не глазами: пунктов шестнадцать (grep -cE '^### 4\.' ), и «кажется, всё» здесь не работает. Заказов ВНЕ §4 нет — всё, что раньше жило в приложениях и в хвостах, вынесено в §4 отдельными номерами (§4.14, §4.15); приложения несут только справку и пинги.

8. Заявление = команда

Каждое число и каждая категорика в отчёте — с командой, которой получены, прямо рядом. Не по памяти о том, как ты это выяснял. Приёмка пере-ранит выборочно; расхождение отчёта с пере-раном дороже, чем честное «не проверял».

⚠ Прошлый отчёт дал повод: его замер цены глагола (5000 → 1.60с/133МБ · 20000 → 9.08с/350МБ) при пере-ране приёмки сошёлся по времени на 5k/10k и разошёлся вдвое по ПАМЯТИ (199/395/798 МБ), а выведенный из него потолок «до ~80k термов в 60-секундный бюджет платформы» оказался оптимистичен примерно вдвое. Числа, которые поедут в чужой пак как датум, стоят отдельной аккуратности.

9. Эхо-протокол старта

ДО работы — не больше десяти строк: скоуп как ты его понял · инварианты, которые не тронешь · что делать НЕ будешь. Расхождение с этим промтом видно сразу и стоит дёшево.

10. Что НЕ удалось

Обязательная секция отчёта. «Не проверено» ≠ «работает». Вердикт о рантайме без живого прогона — максимум PLAUSIBLE, и так и пиши.

11. Канал вопросов

Конфликт промта с кодом или доками — пинг через владельца, не интерпретация. Ты вправе сказать «этого делать не надо» с аргументом: пак, принёсший развилку вместо тихой девиации, — хороший пак. Четыре пункта выше (§4.6, §4.9, §4.10, §4.13) прямо приглашают такой ответ.


Приложение: что приёмка ПОДТВЕРДИЛА — не чини это заново

Чтобы дофикс не тратил время на уже доказанное:

  • Цепь работает (§4.12) · мины 1 и 2 строки 199 обезврежены (явный status: approved пишется; approve без dst отвергается).
  • §4.1§4.7 прежнего заказа исполнены полностью, все девять семантик §4.2 — тоже, семь пунктов закона — тоже; скоуп-крипа нет. Пять вещей сверх заказа (класс 14, export_version, пакет standdata, строгость reject-читателя, два поля отчёта) — аргументированы и безопасны.
  • Ни один платформенный канал не ломается: аддитивные поля прошли через её собственные декодеры на реальных документах; строгих декодеров у неё нет ни одного на пути движковых документов; id термов банка не сдвинулись; Refused(14) истинно, DeploymentFault 14 не покрывает, а интейк отправляет нераспознанный код полосы в ветку, которая файл пользователя НЕ удаляет.
  • Строгость не сломала ни один живой документ корпуса — 44 из 44 (29 сид-схемы + 15 reject-схемы).
  • Критерий §4.6 ПРЕЖНЕГО заказа (дефолт корпуса через standdata) выполнен буквально — не путать с §4.6 ЭТОГО промта, который про другое; §13 сходится точно (59 тест-функций + 2 фаззера, удалено 0); все 28 пинов, названных отчётом, существуют ровно по одному разу.
  • $0 доказан структурно и исполнением; ледджер и база не сдвинулись ни на байт.
  • Три заявки ревьюеров приёмка ОПРОВЕРГЛА, не поднимай их заново: src, нормализующийся в пустой ключ, ловится дедупом поверхностей · повторный decline корректно отчитывается already_applied · отказ по алиасу подписанного сид-терма отвергается по имени.

Приложение: пинги, которые дофикс НЕ исполняет

Названы, чтобы ты их не «допилил попутно»:

  • exit 2 при OOM читается платформой как успех (§4.5) — общедвижковый класс, не твой скоуп.
  • readEngine платформы выбрасывает stdout при любом ненулевом коде, кроме 2а отчёт bank-apply при exit 14 и есть продукт вызова. Требование к ПЛАТФОРМЕННОМУ паку.
  • Развилка «нужен ли redrive канал ключей аргументом» — остаётся пингом. Заказом стала только вторая половина этого пинга — неполный довод в тексте отказа, §4.14.
  • rubyToCandidates мёртв в проде · гейт TestDispatchCommandsCoversTheSwitch слеп к case с константой (⚠ эта слепота названа в §4.11 (в) как ловушка при копировании техники — не унаследуй её) · схема сида без omitempty — три пинга твоего же §10, остаются пингами.
  • Проекция берёт ТОТ ЖЕ лок, что и применение (§10 п.4) ⇒ «поправить термин при живом прогоне» даёт 12 даже на ПРЕВЬЮ, на всю длину прогона. Проектируется ПЛАТФОРМЕННЫМ паком (его окно — состояние между прогонами, когда движок вышел и лок отпущен), не движковым.
  • Опубликованные пути двух файлов решений читаются как приглашение их парсить (§10 п.5) — ⚠ см. §4.6: изгородь снимается сознательно и по ратифицированному направлению закона.
  • Карта подписи не удаляется после погасшего стопа (§4.12) — пред-существующее, пинг.

Записка от бэкенд-сессии ПАКА (написана до компакта её контекста, 25.08)

Я — та сессия, чей пак не приняли. Промт я прочитал целиком и верифицировал исполнением ключевые клеймы, прежде чем писать это. Ниже не инструкции: у тебя есть преимущество, которого нет у меня — взгляд с нуля, и я не хочу его портить. Здесь только то, что дорого узнавать заново.

Что я ПЕРЕПРОВЕРИЛ САМ и подтвердил (не трать на это время)

Воспроизведено моими прогонами на этом же дереве: §4.1 блокер (note с ведущим \n у decline → записывается note: |4-, seed.DecodeRejects не читает) · §4.2 (починка ОДНОЙ из двух битых строк → отказ, обеих сразу → exit 0) · §4.4 (все три формы обходят гард: --decisions=, --decisions "", --keys-file=) · §4.7 (approve 方源 + decline 方小子 одним вызовом → оба applied, rejected: []) · §4.8 (окно [90,3] принимается).

⚠ Единственное место, где промт ШИРЕ замера — проверь до того, как строить фикс

§4.1 говорит, что корень «бьёт по ЛЮБОМУ свободному текстовому полю обоих документов». Мой замер этого не показывает. Прогнал пять форм значения через рендер и обратно, оба документа:

note="\nтекст"    дельта=ломается  rejects=ломается
note="текст\n"    дельта=ок        rejects=ок
note="a\nb"       дельта=ок        rejects=ок
note=" пробел"    дельта=ок        rejects=ок
note="\tтаб"      дельта=ок        rejects=ок

То есть триггер — ведущий перевод строки, а не «любой свободный текст» и не пробельность вообще. Отдельно: dst и sense с тем же ведущим \n у меня round-trip НЕ сломали, а note — сломал, и я это не объяснил. Практический вывод, и он усиливает заказ, а не отменяет его: не пытайся перечислить ломающие входы — их множество я измерил и оно не совпало с интуицией промта. Чини ИНВАРИАНТ (round-trip на обоих документах), а не набор полей. Заплатка на «поля с ведущим \n» будет выглядеть рабочей и промахнётся по следующей форме.

Инструмент, которого у тебя не будет, а он нужен

Харнесс мутаций жил в скретчпаде сессии, не в репозитории. Он умер вместе с ней. Тебе придётся писать его заново, и §4.11 требует десять посадок. Совет: сразу закладывай снятие sha256 целей до прогона и проверку после — у меня прогон убило таймаутом, finally не отработал, посадка просидела в дереве ~40 минут, выглядя продакшн-кодом, и сторонний ревьюер снял с неё замеры и доложил несуществующий дефект. Поймал её мой же пин. Подумай, не место ли такому харнессу в репозитории — приёмка теперь тоже сажает мутации, а инструмент каждый раз пишется с нуля.

Три вещи, на которых я обжёгся, и они повторятся

  1. Одиночный прогон — не замер. Атрибуцию флейков я дважды вывел из одного прогона и оба раза получил противоположный ответ. Решает только чередующийся A/B на двух деревьях (git worktree на HEAD — рабочее дерево при этом не трогается, checkout поверх грязного запрещён).
  2. Субагенты пишут в твоё дерево. Ревью-агенты оставили zz*/*scratch*/refuter_* в internal/, часть удалили за собой, часть нет; один раз это уронило батарею. Сверяйся git ls-files --others перед каждым «финальным» прогоном, и не грепай по zz — попадёшь в Fuzz.
  3. Красное в internal/store — почти наверняка не ты (два -race-флейка, §5 промта). Но проверь это ЗАМЕРОМ против HEAD, а не поверь мне на слово: я один раз получил обратную картину.

Мои сомнения по заказу — там, где §11 приглашает спорить

  • §4.9 (честность отчёта про непогасший стоп). Я не уверен, что дверь это ЗНАЕТ. Она видит, сколько решений применила, но не сколько поверхностей осталось за капом реверс-секции — это знает майнер на следующем проходе. Скорее всего честное поле звучит как «этот вызов решил N; погаснет ли стоп, определит следующий майнинг», а не как предсказание. Если придёшь к тому же — это законный ответ.
  • §4.10 (SIGTERM) и §4.5 (кап) взаимодействуют. На реальной книге вызов — десятки миллисекунд; лок держится долго только на патологически большом документе, который §4.5 и запрещает. Возможно, кап делает половину проблемы несуществующей, и тогда обработчик сигнала — механизм без носителя. Замерь длительность ПОСЛЕ капа, прежде чем решать.
  • §4.6 (пути в manifest --json). Заказ разумен по потребителю, но семантически манифест — про дерево глав, и путь артефакта там гость. Я бы это сделал и назвал компромисс вслух, а не молча.
  • §4.13 «note нельзя очистить» — реши осознанно. Сейчас пустой note в решении означает «не трогай», и это было намеренно (иначе каждое решение стирало бы провенанс владельца). Обратное поведение тоже защитимо; плохо только не назвать.

Про числа

Мой замер памяти (133/217/350 МБ) разошёлся с приёмочным (199/395/798 МБ) примерно вдвое. Я не знаю, кто прав, и не выяснил — методику я не зафиксировал (/usr/bin/time -f %M, машина под нагрузкой). Не наследуй ни моё, ни их: пере-мерь со своей названной методикой, потому что это число поедет в платформенный пак как датум для таймаута.

Главный урок, если брать один

Я потратил два адверсариальных ревью на рассуждение — они нашли настоящие дефекты, но дефект, который стоил бы ДЕНЕГ (--decisions принимался всеми командами), нашёлся, когда я перепроверил собственный скоуп исполнением. А §4.11 этого промта — восемь мест, где мой гейт оказался слепым, и я узнал об этом от приёмки. Отсюда единственный совет по методу: сначала посади мутацию, потом пиши пин. Пин, написанный первым, почти всегда проверяет то, что и так верно.

— бэкенд-сессия пака, до компакта

Онбординг в САМ КОД — этого нет ни в промте, ни в FINDINGS, а искать заново дорого

Дверь двухслойная, и порядок чтения не произвольный:

  1. internal/membank/decisions.goчистая логика, без I/O, часов и рандома. Здесь вся семантика: что принимается, что отвергается, чем отказ обоснован. Сюда садятся §4.1, §4.2, §4.7, §4.8 и бо́льшая часть §4.11.
  2. internal/pipeline/bankdecisions.goобвязка: лок, чтения, два гейта записи на файл, отчёт. Сюда садятся §4.5 (кап), §4.10 (сигнал), §4.6 (если решишь трогать status/manifest).
  3. cmd/tmctl/bankapply.go — тонкий принт отчёта. cmd/tmctl/invocation.go — §4.4 и §4.14.

Обратный порядок даёт ложное впечатление, что дверь про файлы. Она про решения; файлы — следствие.

Карта «предмет → где пин» (пригодится, когда будешь чинить и захочешь понять, что сломал): membank/decisions_test.go — семантика решений · membank/decisions_fuzz_test.go — round-trip и оракулы набора · pipeline/bankdecisions_test.go — лок, гейты записи, классы отказа, «прогон читает конвенционный файл» · pipeline/statusartifacts_test.goartifacts{}/версии · config/decisionpaths_test.go — конвенционные дефолты · cmd/tmctl/keysfile_test.go — гарды флагов и e2e через реальный бинарь · seed/decode_test.go — строгость и пустые документы.

Инварианты, которые легко сломать ПОПУТНО, чиня §4 (у каждого сегодня есть пин, кроме отмеченных в §4.11 как слепые):

  • res.DeltaTouched / res.RejectsTouched — гейты на файл, не на вызов. Их смысл: decline не должен пере-рендеривать дельту, которой не касался. Если будешь трогать applyOne, следи, чтобы флаг ставился ровно тем действием, которое реально изменило документ.
  • newDocumentProblems сравнивает до/после, и это единственное, что не даёт пред-существующей поломке блокировать починку. §4.2 просит поменять КЛЮЧ сравнения (субъект вместо текста), а не выкинуть сравнение.
  • withRubyAliases делает глубокую копию алиасов: AttachRubyAliasesToManual мутирует на месте, а входные строки сида читаются ещё раз в «до». Мелкая копия ломает сравнение до/после незаметно.
  • mergeTerm мержит на существующую строку, а не заменяет её — иначе решение о dst стирает рукописный gender, которого во входном словаре двери нет вовсе.

Как заново собрать живую пробу на стенде (я потратил на это несколько заходов вслепую)

Копия ОБЯЗАТЕЛЬНА вне рабочего дерева; в books/ не писать. Три ловушки, каждая стоила мне прогона:

  1. cp -r books/gu-zhenren/minirun-verify2 <scratch>/live — внутри все пути абсолютные и мёртвые (/home/ubuntu/..., машина переехала). Переписывать надо в ТРЁХ файлах: book.yaml, pipeline.yaml и pairs/zh-ru.yaml — в последнем лежит prompts_root, и про него забываешь, потому что промпты «вроде рядом».
  2. База стенда — схема v11, бинарь ждёт v15. Первый же вызов даёт exit 13. Это НЕ дефект: сделай tmctl migrate --config <копия>/book.yaml на копии. (Заодно это единственный способ увидеть ратифицированный класс 13 живым — юнит-тесты всегда создают базу свежей.)
  3. Банк-стоп на этой машине не воспроизводитсяpipeline.yaml требует jieba-контраст, которого в клоне нет по построению (строка 123). Для цепи §4.12 используй фикстуру internal/pipeline/miningstop_join_test.go, а не стендовую книгу: приёмка прямо говорит, что харнесс там уже есть и цепь на нём гоняется за $0.

Чего в моей памяти было, а в документах нет — выгружаю здесь

  • Почему sameTerm сравнивает YAML-рендер, а не reflect.DeepEqual: вопрос функции — «изменится ли ФАЙЛ», а DeepEqual различает nil и пустой слайс, чего у формата нет, и дал бы ложное «изменилось». Это в комментарии кода есть, но если будешь «упрощать» — знай, что это не небрежность.
  • Почему словарь решений именно такой (note принимается и не публикуется, aliases публикуются и не принимаются): развёрнуто в комментарии Decision. Короткий слоган «вход = выход» НЕВЕРЕН, и «починка» кода под слоган сломает дверь в обе стороны.
  • Класс 14 заводился с проверкой чужой стороны: DeploymentFault платформы покрывает 10/12/13 одинаково, а зовут её только пути интейка — поэтому мис-классификация была перспективной, а не живой. Если будешь трогать полосу, эта проверка делается одним грепом по platform/internal/ingest/exit.go.
  • make battery не детерминированно зелёная (~1 падение на 24 полных -race прогона, всегда в одном из двух названных store-тестов). Первый абзац §1 FINDINGS про это; прочти ДО первого прогона, иначе отнесёшь красное на себя.

— бэкенд-сессия пака, до компакта (дополнение)