Files
jellybit/docs/adr/ADR-2026-08-10-reason-computed-on-read.md
T
av b9f0929d0c layout: непомещающееся целевое имя уводит задачу в review вместо failed
- предел длины компонента (255 байт) проверяется в BuildLinks до первой
  операции с ФС: ни каталога, ни ссылки при отказе не создаётся
- причина пустого предпросмотра считается на показе (ReviewData.PreviewError)
  и печатается в панели действий и в карточке Telegram: у задачи без
  записанной причины взять её больше неоткуда
2026-08-10 12:16:44 +03:00

6.0 KiB

Причина, по которой человек не видит плана, считается на показе, а не читается из состояния

Контекст

Проверка длины целевого имени встала в layout.BuildLinks — туда же, где собираются оба предпросмотра экрана ревью. Это дало даром совпадение показанного с применённым, но и вторую половину: непомещающееся имя обнуляет предпросмотр, а без предпросмотра экран прячет команду «Применить».

Первым решением панель действий брала текст из error_msg — причины, записанной при последнем переходе. Ревью показало, что на самом частом входе этого поля нет вовсе: задача, пришедшая в review из-за отсутствия матча, попадает туда с пустой причиной и до раскладки не доходит. Замер триажа на двух деревьях:

до изменения:  предпросмотр строится, «Применить» доступна,
               применение доводит до failed с текстом ядра
после:         предпросмотр пуст, команды нет, причины нет —
               экран печатает «Подтверди источник», хотя источник ни при чём

То есть изменение, чья цель — «человек узнаёт причину», на этом входе диагностируемость ухудшало.

Решение

Причина отказа считается в момент показа и отдаётся транспорту значением (worker.ReviewData.PreviewError); записанная в состоянии используется только когда посчитанной нет.

Цитата из design.md, Решение 6:

Оба предпросмотра ревью (карточка и строка источника) строят пути тем же BuildLinks, поэтому вердикт на показе и вердикт на применении совпадают по устройству, а не по договорённости.

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

Чтение при этом состояние не двигает. Построение предпросмотра остаётся без побочных эффектов; причина уходит наружу возвращаемым значением.

Это второй случай одного класса за день. Первый — ADR-2026-08-10-sanitize-at-every-entry: гарантия, поставленная на запись, не покрывает то, что записано раньше. Здесь она не покрывает то, что не записано вовсе.

Рассмотренные варианты

  • Записывать причину в состояние при построении предпросмотра. Отвергнуто: чтение начало бы двигать состояние. Запрет уже стоял в коде отдельным комментарием — предпросмотр не переводит задачу в review при рассинхроне папок, — и заводить исключение ради текста на экране значило бы снять правило.
  • Оставить как есть, записав остаток сценарием спеки. Отвергнуто на чекпоинте: регресс диагностируемости дошёл бы до боевого окружения на самом частом входе.
  • Печатать причину только в баннере состояния, панель не трогать. Отвергнуто: баннер показывает записанное и на этом входе пуст ровно так же.

Цена

Причина живёт в двух местах — записанная в состоянии и посчитанная на показе, — и порядок между ними держится на ревью, а не на типе. Взамен экран ревью объясняет отсутствие команды всегда, а не только когда причину успели записать, и объяснение относится к текущему плану, а не к прошлому.

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