layout: непомещающееся целевое имя уводит задачу в review вместо failed

- предел длины компонента (255 байт) проверяется в BuildLinks до первой
  операции с ФС: ни каталога, ни ссылки при отказе не создаётся
- причина пустого предпросмотра считается на показе (ReviewData.PreviewError)
  и печатается в панели действий и в карточке Telegram: у задачи без
  записанной причины взять её больше неоткуда
This commit is contained in:
av
2026-08-10 12:16:44 +03:00
parent 1710e5a9d5
commit b9f0929d0c
31 changed files with 1511 additions and 17 deletions
+10
View File
@@ -171,6 +171,11 @@ func (l *Layouter) BuildLinks(p Plan) ([]Link, error) {
if !underRoot(root, dst) {
return nil, fmt.Errorf("layout: target %q is outside library %q (file %q)", dst, root, f.Src)
}
// Длина проверяется ПОСЛЕ песочницы: путь, вышедший за библиотеку, —
// находка безопасности, и подменять её косметической причиной нельзя.
if err := checkComponentLengths(root, dst); err != nil {
return nil, err
}
links = append(links, Link{Src: f.Src, Dst: dst, Kind: kind})
}
if len(links) == 0 {
@@ -267,6 +272,11 @@ type Result struct {
// ErrCollision — цель существует и это другой файл (нужен review).
var ErrCollision = errors.New("layout: target collision")
// ErrNameTooLong — компонент целевого пути длиннее предела длины имени
// (maxComponentBytes). Проверяется в BuildLinks, до первой операции с ФС:
// задача уходит в review с доменной причиной, а не в failed с текстом ядра.
var ErrNameTooLong = errors.New("layout: имя не помещается")
// ErrLastCopy — отказ снять ссылку, которая осталась последней копией данных
// (источник недоступен или nlink<=1). Undo снимает лишний хардлинк, а не
// единственный файл (см. state-reconciliation, инвариант безопасного Undo).