Раскладка: сходимость папки сериала (второй сезон в ту же папку)

При подтверждённом матче база папки (имя+год) наследуется от живой
папки-якоря того же (provider, provider_id) вместо печати заново из выхода
LLM — так второй/последующий сезон ложится в ТУ ЖЕ папку, а не заводит
рядом почти одинаковую. Отдельная сущность «тайтл» не вводится.

- layout: Plan.FolderBase перекрывает базу в папке и именах файлов;
  TitleFolder разбирает dst_path в папку тайтла и базу (снятие тега).
- store: LiveTitleFolders — dst_path живых ссылок того же матча.
- worker: resolveFolderBase (живость якоря — по наличию папки на диске,
  os.Lstat, а не по статусу ссылки в БД) в linkPlan и в превью ревью
  (превью=применение); рассинхрон нескольких живых папок → review из
  linking (deferred→review в графе нет).

Схема БД не менялась. Change series-folder-convergence заархивирован,
требования влиты в openspec/specs/file-layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-10 13:50:49 +03:00
co-authored by Claude Opus 4.8
parent 3f1a928000
commit 5c3ef79496
20 changed files with 1031 additions and 56 deletions
+26
View File
@@ -293,6 +293,32 @@ func (s *Store) LayoutSizeByDownload(ctx context.Context, ids []string) (map[str
return out, nil
}
// LiveTitleFolders возвращает целевые пути (dst_path) живых ссылок
// (`status IN linked/copied/exists`) загрузок, чьё current recognition имеет тот
// же (provider, provider_id), КРОМЕ excludeDownloadID. Питает правило сходимости
// папки (см. file-layout): вызывающий сводит пути к папкам тайтла, проверяет их
// существование на диске и наследует базу имени. Пустой provider/id → пусто (без
// подтверждённого матча якоря нет). Проверка живости папки на диске —
// ответственность вызывающего (статус ссылки не гарантирует наличие папки).
func (s *Store) LiveTitleFolders(ctx context.Context, provider, providerID, excludeDownloadID string) ([]string, error) {
if provider == "" || providerID == "" {
return nil, nil
}
const q = `
SELECT fl.dst_path
FROM file_link fl
JOIN recognition r ON r.download_id = fl.download_id AND r.is_current = 1
WHERE fl.status IN ('linked', 'copied', 'exists')
AND fl.download_id != ?
AND r.provider = ?
AND r.provider_id = ?`
var out []string
if err := s.DB.SelectContext(ctx, &out, q, excludeDownloadID, provider, providerID); err != nil {
return nil, fmt.Errorf("live title folders: %w", err)
}
return out, nil
}
// DeleteFileLinksByBatch удаляет записи ссылок батча (после undo на ФС).
func (s *Store) DeleteFileLinksByBatch(ctx context.Context, batchID string) error {
if _, err := s.DB.ExecContext(ctx,