fix(migrator): heal 2.44.0 stacks with StackFileVersion>0 but missing v{N} dir (#30) #31

Merged
vvzvlad merged 2 commits from fix/30-migration-heal into develop 2026-07-03 01:33:37 +03:00
Collaborator

Summary

Чинит #30: миграция 2.44.0 слепо скипала стеки с StackFileVersion > 0, считая что версионированная раскладка compose/{id}/v{N} на диске уже есть. Это неверно для стеков, чьи файлы лежат только в плоском базовом пути (compose/{id}/<entrypoint>) — из-за EE↔CE juggling (--rollback-to-ce), restore БД, старых билдов. Такие стеки пропускались и оставались сломанными: GET /stacks/{id}/file?version=N → 500 (каталога v{N} нет), при этом сам стек продолжает работать. closes #30.

Заменил слепой guard проверкой РЕАЛЬНОГО состояния диска текущей версии: полный v{N} → adopt (идемпотентный no-op, если уже перенаведён); отсутствует/неполный → heal, материализуя v{N} из плоских базовых файлов. Пути fresh (v==0→v1) и heal (v>0-но-отсутствует) вынесены в общий materializeStackVersion; seedStackVersionMetadata обобщён на версию v (стек с StackFileVersion=3 лечится в v3, не в v1; Versions сидится только когда пусто — реальная история не затирается).

How verified

  • go build ./... — 0; go vet ./api/datastore/migrator/... — чисто; go test ./api/datastore/migrator/... — зелёные.
  • Новые тесты (все PASS): HealFlatV1 (репро #30: StackFileVersion=1, плоский ProjectPath, base есть, v1/ нет → создаётся v1/docker-compose.yml с идентичным контентом, ProjectPath→v1, base удалён, идемпотентно на повторе), HealFlatV3 (обобщение: heal в v3, v1 не создаётся), CompleteExistingVersionNoDoubleWrite (готовый v{N} + реальная история не тронуты). Существующие MultiFileMove/SkipsGitAndOrphan/MissingAdditionalFile/IncompleteExistingV1 сохранены и зелёные.
  • Внутреннее ревью (data-safety, 8 инвариантов) — APPROVE: all-or-nothing (все файлы читаются в память до записи; частичный v{N} не адаптируется и не пишется), удаление базовых копий ТОЛЬКО после репойнта ProjectPath (крэш/деплой mid-migration не находит пропавший файл — повторный прогон достраивает из полного v{N}), идемпотентность, сохранение истории, git/kube skip.

Scope

Per issue: одна инстанция форка, её единственный затронутый стек (timeseries) уже пропатчен руками — self-heal-проход не нужен, достаточно правки guard'а (EE-серверы, которые переедут позже, ещё не гоняли 2.44.0 и подхватят исправленную версию).

Checklist

  • критерии из #30 (стек с версией>0 без v-dir лечится; retrieval by version работает)
  • all-or-nothing / без частичного v{N}; base удаляется только после репойнта
  • идемпотентность + сохранение реальной истории; git/kube не затронуты
## Summary Чинит #30: миграция 2.44.0 слепо скипала стеки с `StackFileVersion > 0`, считая что версионированная раскладка `compose/{id}/v{N}` на диске уже есть. Это неверно для стеков, чьи файлы лежат только в плоском базовом пути (`compose/{id}/<entrypoint>`) — из-за EE↔CE juggling (`--rollback-to-ce`), restore БД, старых билдов. Такие стеки пропускались и оставались сломанными: `GET /stacks/{id}/file?version=N` → 500 (каталога `v{N}` нет), при этом сам стек продолжает работать. closes #30. Заменил слепой guard проверкой РЕАЛЬНОГО состояния диска текущей версии: полный `v{N}` → adopt (идемпотентный no-op, если уже перенаведён); отсутствует/неполный → heal, материализуя `v{N}` из плоских базовых файлов. Пути fresh (`v==0→v1`) и heal (`v>0`-но-отсутствует) вынесены в общий `materializeStackVersion`; `seedStackVersionMetadata` обобщён на версию `v` (стек с `StackFileVersion=3` лечится в `v3`, не в `v1`; `Versions` сидится только когда пусто — реальная история не затирается). ## How verified - `go build ./...` — 0; `go vet ./api/datastore/migrator/...` — чисто; `go test ./api/datastore/migrator/...` — зелёные. - Новые тесты (все PASS): `HealFlatV1` (репро #30: `StackFileVersion=1`, плоский ProjectPath, base есть, `v1/` нет → создаётся `v1/docker-compose.yml` с идентичным контентом, `ProjectPath→v1`, base удалён, идемпотентно на повторе), `HealFlatV3` (обобщение: heal в `v3`, `v1` не создаётся), `CompleteExistingVersionNoDoubleWrite` (готовый `v{N}` + реальная история не тронуты). Существующие `MultiFileMove`/`SkipsGitAndOrphan`/`MissingAdditionalFile`/`IncompleteExistingV1` сохранены и зелёные. - Внутреннее ревью (data-safety, 8 инвариантов) — APPROVE: all-or-nothing (все файлы читаются в память до записи; частичный `v{N}` не адаптируется и не пишется), удаление базовых копий ТОЛЬКО после репойнта ProjectPath (крэш/деплой mid-migration не находит пропавший файл — повторный прогон достраивает из полного `v{N}`), идемпотентность, сохранение истории, git/kube skip. ## Scope Per issue: одна инстанция форка, её единственный затронутый стек (`timeseries`) уже пропатчен руками — self-heal-проход не нужен, достаточно правки guard'а (EE-серверы, которые переедут позже, ещё не гоняли 2.44.0 и подхватят исправленную версию). ## Checklist - [x] критерии из #30 (стек с версией>0 без v-dir лечится; retrieval by version работает) - [x] all-or-nothing / без частичного v{N}; base удаляется только после репойнта - [x] идемпотентность + сохранение реальной истории; git/kube не затронуты
agent_coder added 1 commit 2026-07-03 00:31:27 +03:00
The 2.44.0 stack-file-version migration skipped any stack with StackFileVersion>0,
assuming the versioned on-disk layout (compose/{id}/v{N}) already existed. That is
false for stacks whose files still live only at the flat base path (from EE<->CE
edition juggling, DB restores, older builds): they were skipped and left broken —
GET /stacks/{id}/file?version=N returns 500 (the v{N} dir doesn't exist), while the
stack keeps running.

Replace the blanket StackFileVersion>0 skip with a check on the ACTUAL on-disk state
of the current version: complete v-dir -> adopt (idempotent no-op when already
repointed); missing/incomplete -> heal by materializing v{N} from the flat base
files. Factor the fresh (v==0->v1) and heal (v>0-but-missing) paths into one shared
materializeStackVersion helper, and generalize seedStackVersionMetadata to an
arbitrary version v (heals a StackFileVersion=3 stack into v3, not v1; seeds Versions
only when empty so a real history is never clobbered).

Preserves the all-or-nothing guarantee (read every file before writing any; never
write a partial v{N}) and the base-copy-removal ordering (base files are removed only
after ProjectPath is repointed to the v-dir, so a deploy mid-migration or a crash
never finds a missing file — a re-run completes the repoint from the full v{N}).

closes #30

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-03 00:31:28 +03:00
Collaborator

Ревью — #31 (heal 2.44.0 stacks with StackFileVersion>0 but missing v{N} dir), round 1, head 54854f263, base develop (merge-base 5f6e344fe)

Вердикт: CHANGES — фикс #30 корректен и data-safe по чтению (all-or-nothing перед удалением подтверждён, все крэш-окна восстановимы повторным прогоном), но: (F1) главная data-safety-критичная ветка crash-recovery НЕ покрыта тестом, (F2) удаление базовых файлов в новой функции не заклэмплено (traversal-footgun в file-DELETING пути миграции). Оба — в тронутом коде, bounded.

Полный 9-аспектный веер. Go-тулчейна в окружении нет → базис = coder-run (go build/vet/test green по claim) + независимое чтение; 8 аспектов подтверждают чтением, что claim держится (invariants верны). Это МИГРАЦИЯ + удаление файлов → sensitive.

Подтверждено чтением (data-safety держится)

  • Фикс #30 корректен: слепой guard StackFileVersion>0 → skip заменён проверкой реального диска; flat-стек без v{N}materializeStackVersion из базовых файлов. Heal идёт в v{StackFileVersion} (v3 для =3, не хардкод v1). Versions-история сохраняется (сидится только при len==0). Adopt полного v{N} — идемпотентный no-op (guard ProjectPath==vDir && StackFileVersion==v && len(Versions)>0).
  • All-or-nothing перед удалением — ПОДТВЕРЖДЕНО: все базовые файлы читаются в память (:139-148), затем пишутся все в v{N} (:151-155), и ТОЛЬКО потом удаляется база (:162-167). Нет порядка, где база удаляется при частичном v{N}. Ошибка чтения → skip (база цела); ошибка записи → abort (база цела); ошибка os.Remove базы → лог, не фатально (v{N} уже полон). Крэш в любом окне восстановим повторным прогоном через adopt-ветку (которая базу НЕ удаляет).
  • Плюс (goal-fit win): materialize читает из канонического GetStackProjectPath(stackID), а не из DB stack.ProjectPath — строго корректнее для DB-restore-кейса (устаревший абсолютный ProjectPath из чужого хоста), а это одна из причин #30.
  • Регрессий нет (fresh v0→v1 байт-идентичен; SkipsGitAndOrphan/MultiFileMove/MissingAdditionalFile/IncompleteExistingV1 сохранены). Docs точны (старый skip-коммент удалён). Conventions идиоматичны.

Do — apply these, then re-review

  • F1 [test-coverage, blocking — data-safety-критичная ветка без покрытия]migrate_2_44_0.go:95-98 (adopt-ветка: полный v{N} на диске, но DB-метаданные устаревшие → repoint+persist). Это РОВНО состояние после крэша между материализацией (v{N} записан, база удалена, :162) и DB-персистом (:114): база уже удалена, DB ещё указывает на базу. Вся история crash-safety опирается на то, что повторный прогон это чинит — но НИ ОДИН тест сюда не заходит: все существующие тесты, попадающие в complete-v-dir ветку, имеют уже-консистентные метаданные → садятся на no-op continue (:92). Fix: тест — засидить полный v{N} через StoreStackFileFromBytesByVersion, создать стек с ProjectPath = GetStackProjectPath(id) / пустым Versions, удалить базовые копии, прогнать миграцию, заассертить: ProjectPath перенаведён на v{N}, StackFileVersion==v, Versions засижен, И контент v{N} цел. (Харнесс newStackVersionTestMigrator уже есть.)
  • F2 [security, low — незаклэмпленное удаление в file-DELETING пути]migrate_2_44_0.go:163-164. Удаление базы: os.Remove(filepath.Join(basePath, fileName)). filepath.Join (в отличие от JoinPaths, filesystem.go:87) НЕ клэмпит traversal, а fileName = stack.EntryPoint/AdditionalFiles. Чтение и запись в ЭТОЙ ЖЕ новой функции идут через заклэмпленные GetFileContent/StoreStackFileFromBytesByVersion — удаление осталось единственной незаклэмпленной мутацией. Эксплойт контрив (нужен ../ в fileName + staged-файл в заклэмпленной read-локации; популяция — DB-restore/import стеки), паттерн частично пред-существовал — но это удаление в миграции, и фикс однострочный: удалять через JoinPaths(basePath, fileName), консистентно с read/write этой же функции. Заклэмпь.

DROP — кодеру НЕ делать · калибровочный лог (для оператора)

  • [below-threshold] low [stability] stackVersionDirComplete проверяет только существование, не контент/размер: v{N}/file, обрезанный мид-io.Copy крэшем (нет fsync), пройдёт как complete → adopt на усечённый. НО база выживает (adopt её не удаляет) → нет перманентной потери; совпадает с не-атомарной нормой кодовой базы. Опц.-харден: content-hash/atomic temp+rename.
  • [speculative] low [stability] no-fsync power-loss между записью v{N} и удалением базы может потерять обе копии — пред-существующее (все stack-file-операции portainer), process-crash (частый случай) полностью восстановим. Не этот PR.
  • [below-threshold] info [coherence] частичный пред-существующий v{N} (v>0) НЕ лечится (skip, FileExists(vDir)==true навсегда) — узкий вариант #30 остаётся с 500; data-safe (базу не трогает), консервативный scope, docstring честен. Лечить = write-risk, оставить defensible.
  • [out-of-scope] info [architecture] heal живёт в one-shot version-gated миграции — не перезапустится при повторном дрейфе после 2.44.0 (будущий restore pre-2.44 бэкапа → снова 500). Постоянно закрыл бы idempotent repair-on-startup. Отдельный трек, не гейт.
  • [note] [conventions] bare return err (без stack-id контекста) — доминирующий стиль пакета (2_43 — исключение с %w); не дефект.
## Ревью — #31 (heal 2.44.0 stacks with StackFileVersion>0 but missing v{N} dir), round 1, head `54854f263`, base develop (merge-base `5f6e344fe`) **Вердикт: CHANGES** — фикс #30 корректен и data-safe по чтению (all-or-nothing перед удалением подтверждён, все крэш-окна восстановимы повторным прогоном), но: (F1) главная data-safety-критичная ветка crash-recovery НЕ покрыта тестом, (F2) удаление базовых файлов в новой функции не заклэмплено (traversal-footgun в file-DELETING пути миграции). Оба — в тронутом коде, bounded. Полный 9-аспектный веер. **Go-тулчейна в окружении нет** → базис = coder-run (go build/vet/test green по claim) + независимое чтение; 8 аспектов подтверждают чтением, что claim держится (invariants верны). Это МИГРАЦИЯ + удаление файлов → sensitive. ### Подтверждено чтением (data-safety держится) - **Фикс #30 корректен:** слепой guard `StackFileVersion>0 → skip` заменён проверкой реального диска; flat-стек без `v{N}` → `materializeStackVersion` из базовых файлов. Heal идёт в `v{StackFileVersion}` (v3 для =3, не хардкод v1). `Versions`-история сохраняется (сидится только при `len==0`). Adopt полного `v{N}` — идемпотентный no-op (guard `ProjectPath==vDir && StackFileVersion==v && len(Versions)>0`). - **All-or-nothing перед удалением — ПОДТВЕРЖДЕНО:** все базовые файлы читаются в память (`:139-148`), затем пишутся все в `v{N}` (`:151-155`), и ТОЛЬКО потом удаляется база (`:162-167`). Нет порядка, где база удаляется при частичном `v{N}`. Ошибка чтения → skip (база цела); ошибка записи → abort (база цела); ошибка `os.Remove` базы → лог, не фатально (v{N} уже полон). Крэш в любом окне восстановим повторным прогоном через adopt-ветку (которая базу НЕ удаляет). - **Плюс (goal-fit win):** materialize читает из канонического `GetStackProjectPath(stackID)`, а не из DB `stack.ProjectPath` — строго корректнее для DB-restore-кейса (устаревший абсолютный ProjectPath из чужого хоста), а это одна из причин #30. - Регрессий нет (fresh v0→v1 байт-идентичен; SkipsGitAndOrphan/MultiFileMove/MissingAdditionalFile/IncompleteExistingV1 сохранены). Docs точны (старый skip-коммент удалён). Conventions идиоматичны. ### Do — apply these, then re-review - **F1 [test-coverage, blocking — data-safety-критичная ветка без покрытия]** — `migrate_2_44_0.go:95-98` (adopt-ветка: полный `v{N}` на диске, но DB-метаданные устаревшие → repoint+persist). Это РОВНО состояние после крэша между материализацией (`v{N}` записан, база удалена, `:162`) и DB-персистом (`:114`): база уже удалена, DB ещё указывает на базу. Вся история crash-safety опирается на то, что повторный прогон это чинит — но НИ ОДИН тест сюда не заходит: все существующие тесты, попадающие в complete-v-dir ветку, имеют уже-консистентные метаданные → садятся на no-op `continue` (`:92`). Fix: тест — засидить полный `v{N}` через `StoreStackFileFromBytesByVersion`, создать стек с `ProjectPath = GetStackProjectPath(id)` / пустым `Versions`, удалить базовые копии, прогнать миграцию, заассертить: `ProjectPath` перенаведён на `v{N}`, `StackFileVersion==v`, `Versions` засижен, И контент `v{N}` цел. (Харнесс `newStackVersionTestMigrator` уже есть.) - **F2 [security, low — незаклэмпленное удаление в file-DELETING пути]** — `migrate_2_44_0.go:163-164`. Удаление базы: `os.Remove(filepath.Join(basePath, fileName))`. `filepath.Join` (в отличие от `JoinPaths`, `filesystem.go:87`) НЕ клэмпит traversal, а `fileName` = `stack.EntryPoint`/`AdditionalFiles`. Чтение и запись в ЭТОЙ ЖЕ новой функции идут через заклэмпленные `GetFileContent`/`StoreStackFileFromBytesByVersion` — удаление осталось единственной незаклэмпленной мутацией. Эксплойт контрив (нужен `../` в fileName + staged-файл в заклэмпленной read-локации; популяция — DB-restore/import стеки), паттерн частично пред-существовал — но это удаление в миграции, и фикс однострочный: удалять через `JoinPaths(basePath, fileName)`, консистентно с read/write этой же функции. Заклэмпь. --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора) - `[below-threshold]` `low` **[stability]** `stackVersionDirComplete` проверяет только существование, не контент/размер: `v{N}/file`, обрезанный мид-`io.Copy` крэшем (нет fsync), пройдёт как complete → adopt на усечённый. НО база выживает (adopt её не удаляет) → нет перманентной потери; совпадает с не-атомарной нормой кодовой базы. Опц.-харден: content-hash/atomic temp+rename. - `[speculative]` `low` **[stability]** no-fsync power-loss между записью `v{N}` и удалением базы может потерять обе копии — пред-существующее (все stack-file-операции portainer), process-crash (частый случай) полностью восстановим. Не этот PR. - `[below-threshold]` `info` **[coherence]** частичный пред-существующий `v{N}` (v>0) НЕ лечится (skip, `FileExists(vDir)==true` навсегда) — узкий вариант #30 остаётся с 500; data-safe (базу не трогает), консервативный scope, docstring честен. Лечить = write-risk, оставить defensible. - `[out-of-scope]` `info` **[architecture]** heal живёт в one-shot version-gated миграции — не перезапустится при повторном дрейфе после 2.44.0 (будущий restore pre-2.44 бэкапа → снова 500). Постоянно закрыл бы idempotent repair-on-startup. Отдельный трек, не гейт. - `[note]` **[conventions]** bare `return err` (без stack-id контекста) — доминирующий стиль пакета (2_43 — исключение с `%w`); не дефект. <!-- state:review reviewed_head=54854f263f626b792dbe85b9050d6bb204369102 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-03 00:51:02 +03:00
agent_coder added 1 commit 2026-07-03 01:03:07 +03:00
F2 (security): the base-file removal in materializeStackVersion used the unclamped
filepath.Join while the read (GetFileContent) and write
(StoreStackFileFromBytesByVersion) in the same function are clamped. An
attacker-influenceable fileName (stack.EntryPoint/AdditionalFiles via DB
restore/import) could escape basePath in this file-deleting path. Use the clamped
filesystem.JoinPaths, consistent with the read/write.
F1 (test): the adopt branch (complete v{N} on disk but stale DB metadata -> repoint
ProjectPath + set StackFileVersion + seed Versions) is exactly the post-crash state
the crash-safety guarantee relies on (crash between materialize and DB persist), but
no test hit it — existing complete-v-dir tests have consistent metadata and take the
no-op short-circuit. Add TestMigrateStackFileVersions_2_44_0_AdoptCompleteVDirStaleMetadata:
seed a complete v1 on disk, stack with stale flat ProjectPath + empty Versions, no
base copies; assert the migration repoints ProjectPath to v1, sets StackFileVersion=1,
seeds Versions, and leaves the v1 files intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

F1/F2 закрыты, коммит 7b5dfeac.

F2 (security): удаление базовых файлов в materializeStackVersion теперь через заклэмпленный filesystem.JoinPaths(basePath, fileName) вместо filepath.Join — консистентно с read (GetFileContent) и write (StoreStackFileFromBytesByVersion) в той же функции. Для не-traversal fileName путь идентичен старому (basePath = trusted root из GetStackProjectPath), так что поведение не меняется; traversal через ../ в stack.EntryPoint/AdditionalFiles (популяция — restore/import) больше не выходит за basePath в этом file-deleting пути. path/filepath оставил — он ещё используется в stackVersionDirComplete.

F1 (test): добавил TestMigrateStackFileVersions_2_44_0_AdoptCompleteVDirStaleMetadata — покрывает crash-recovery adopt-ветку (полный v{N} на диске, но DB-метаданные устаревшие → repoint), ровно то состояние после крэша между материализацией (v{N} записан + база удалена) и DB-персистом. Сетап: сидю полный v1 через StoreStackFileFromBytesByVersion, стек с ProjectPath = GetStackProjectPath (плоский, устаревший) + пустым Versions + StackFileVersion=0, базовых копий нет. Это НЕ садится на no-op short-circuit (ProjectPath != vDir и len(Versions)==0), а идёт в repoint. Ассерты: ProjectPath перенаведён на v1, StackFileVersion==1, Versions засижен (len 1), оба файла v1 целы/не переписаны, плоская база не воссоздана. Упал бы, если бы adopt-ветка не персистила метаданные.

Проверка: go build 0, go vet чисто, go test ./api/datastore/migrator/... — зелёные, все 8 TestMigrateStackFileVersions_2_44_0_* PASS (вкл. новый). review/needs.

F1/F2 закрыты, коммит `7b5dfeac`. F2 (security): удаление базовых файлов в `materializeStackVersion` теперь через заклэмпленный `filesystem.JoinPaths(basePath, fileName)` вместо `filepath.Join` — консистентно с read (`GetFileContent`) и write (`StoreStackFileFromBytesByVersion`) в той же функции. Для не-traversal fileName путь идентичен старому (`basePath` = trusted root из `GetStackProjectPath`), так что поведение не меняется; traversal через `../` в `stack.EntryPoint`/`AdditionalFiles` (популяция — restore/import) больше не выходит за basePath в этом file-deleting пути. `path/filepath` оставил — он ещё используется в `stackVersionDirComplete`. F1 (test): добавил `TestMigrateStackFileVersions_2_44_0_AdoptCompleteVDirStaleMetadata` — покрывает crash-recovery adopt-ветку (полный `v{N}` на диске, но DB-метаданные устаревшие → repoint), ровно то состояние после крэша между материализацией (`v{N}` записан + база удалена) и DB-персистом. Сетап: сидю полный `v1` через `StoreStackFileFromBytesByVersion`, стек с `ProjectPath = GetStackProjectPath` (плоский, устаревший) + пустым `Versions` + `StackFileVersion=0`, базовых копий нет. Это НЕ садится на no-op short-circuit (`ProjectPath != vDir` и `len(Versions)==0`), а идёт в repoint. Ассерты: `ProjectPath` перенаведён на `v1`, `StackFileVersion==1`, `Versions` засижен (len 1), оба файла `v1` целы/не переписаны, плоская база не воссоздана. Упал бы, если бы adopt-ветка не персистила метаданные. Проверка: `go build` 0, `go vet` чисто, `go test ./api/datastore/migrator/...` — зелёные, все 8 `TestMigrateStackFileVersions_2_44_0_*` PASS (вкл. новый). review/needs.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-03 01:03:28 +03:00
Collaborator

Ре-ревью — #31 (heal 2.44.0 stacks, closes #30), round 2, head 7b5dfeac3, base develop (merge-base 5f6e344fe)

Вердикт: PASS — оба round-1 DO (F1, F2) закрыты, сверено ПО КОДУ. Data-safety цела (all-or-nothing перед удалением, крэш-восстановимо). Do-list пуст, эскалаций нет. Готов к мержу.

Полный 9-аспектный веер (отдельный субагент на аспект) на полном merge-base диффе, спотлайт на round-2-дельте (7 строк .go + новый тест). Go-тулчейна в окружении нет → базис = coder-run (go build 0, go vet чисто, go test ./api/datastore/migrator/... зелёные, все 8 TestMigrateStackFileVersions_2_44_0_* PASS) + независимое чтение; все 9 аспектов подтверждают, что чтение поддерживает claim. Sensitive (миграция + удаление файлов).

Закрыто (сверено по коду)

  • F1 (crash-recovery adopt-ветка без покрытия) — ЗАКРЫТО. Новый TestMigrateStackFileVersions_2_44_0_AdoptCompleteVDirStaleMetadata (migrate_2_44_0_test.go:351-406) воспроизводит точное состояние после крэша между материализацией и DB-персистом: полный v1 на диске, плоская база «удалена» (не засижена), DB устаревшая (ProjectPath=base, StackFileVersion=0, Versions=nil). Трасса подтверждена: v=0→1, vDir=v1 exists+complete → adopt-блок; no-op short-circuit (:93) НЕ срабатывает (ProjectPath != vDir) → repoint+persist (:96-99). Не-вакуозно: три независимых load-bearing ассерта (ProjectPath→v1, StackFileVersion==1, Versions len 1) упали бы, если adopt-ветка не персистит. Плюс ассерты «файлы v1 не переписаны» и «плоская база не воссоздана». Прим.: кодер взял StackFileVersion=0 (не =N) — это ВЕРНЕЕ: ровно faithful post-crash-состояние fresh-пути, и 0→1 transition сам заассерчен (сильнее, чем =N). Ветка выбирается существованием v-dir, не версией → :96-98 покрыт.
  • F2 (незаклэмпленное удаление базы) — ЗАКРЫТО. migrate_2_44_0.go:168: удаление теперь filesystem.JoinPaths(basePath, fileName) вместо filepath.Join — консистентно с заклэмпленными read (GetFileContent) и write (StoreStackFileFromBytesByVersion) в ЭТОЙ ЖЕ функции. JoinPaths клэмпит (filepath.Join(root, filepath.Join("/", untrusted))../ схлопывается против / до join с root). Для нормального fileName путь байт-идентичен старому (basePath trusted root) → нет изменения поведения / пропущенных удалений; traversal через ../ в EntryPoint/AdditionalFiles больше не выходит за basePath в file-DELETING пути. Импорты чисты (filesystem используется :168, path/filepath ещё нужен в stackVersionDirComplete :182 → нет unused-import build-break). Регрессий нет — наоборот, закрыта пред-существовавшая рассогласованность read(clamped)/delete(unclamped).

Do — apply these, then re-review

(нет)


DROP — кодеру НЕ делать · калибровочный лог (для оператора)

  • [style/linter] info [conventions] migrate_2_44_0_test.go:403 строит проверку через filesystem.JoinPaths(basePath, "docker-compose.yml"), тогда как соседние тесты — конкатенацией basePath + "/..."; чище, но единственная стилевая рассогласованность. Косметика.
  • [below-threshold] low [stability, round-1 перенос] stackVersionDirComplete проверяет только существование, не контент/размер: усечённый мид-io.Copy крэшем v{N}/file пройдёт как complete → adopt. База выживает (adopt её не удаляет) → нет перманентной потери; совпадает с не-атомарной нормой кодовой базы. Опц.-харден.
  • [speculative] low [stability, round-1 перенос] no-fsync power-loss между записью v{N} и удалением базы — пред-существующее, process-crash восстановим.

Вне scope — кандидаты на отдельную задачу (NOT part of this verdict)

  • [debt, round-1 перенос] one-shot heal не перезапустится при дрейфе ПОСЛЕ 2.44.0migrate_2_44_0.go — миграция version-gated; будущий restore pre-2.44 бэкапа → снова 500 без авто-починки. Постоянно закрыл бы idempotent repair-on-startup. Отдельный трек (принятый дизайн version-pinned миграции, не форк).
  • [cleanup, round-1 перенос] incomplete pre-existing v{N} (v>0) НЕ лечится (skip навсегда) — узкий вариант #30 остаётся с 500; data-safe, консервативный scope; лечить = write-risk. Отдельный трек.
## Ре-ревью — #31 (heal 2.44.0 stacks, closes #30), round 2, head `7b5dfeac3`, base develop (merge-base `5f6e344fe`) **Вердикт: PASS** — оба round-1 DO (F1, F2) закрыты, сверено ПО КОДУ. Data-safety цела (all-or-nothing перед удалением, крэш-восстановимо). Do-list пуст, эскалаций нет. Готов к мержу. Полный 9-аспектный веер (отдельный субагент на аспект) на полном merge-base диффе, спотлайт на round-2-дельте (7 строк .go + новый тест). **Go-тулчейна в окружении нет** → базис = coder-run (go build 0, go vet чисто, `go test ./api/datastore/migrator/...` зелёные, все 8 `TestMigrateStackFileVersions_2_44_0_*` PASS) + независимое чтение; все 9 аспектов подтверждают, что чтение поддерживает claim. Sensitive (миграция + удаление файлов). ### Закрыто (сверено по коду) - **F1 (crash-recovery adopt-ветка без покрытия) — ЗАКРЫТО.** Новый `TestMigrateStackFileVersions_2_44_0_AdoptCompleteVDirStaleMetadata` (`migrate_2_44_0_test.go:351-406`) воспроизводит точное состояние после крэша между материализацией и DB-персистом: полный `v1` на диске, плоская база «удалена» (не засижена), DB устаревшая (`ProjectPath=base`, `StackFileVersion=0`, `Versions=nil`). Трасса подтверждена: `v=0→1`, `vDir=v1` exists+complete → adopt-блок; no-op short-circuit (`:93`) НЕ срабатывает (`ProjectPath != vDir`) → repoint+persist (`:96-99`). **Не-вакуозно**: три независимых load-bearing ассерта (`ProjectPath→v1`, `StackFileVersion==1`, `Versions` len 1) упали бы, если adopt-ветка не персистит. Плюс ассерты «файлы `v1` не переписаны» и «плоская база не воссоздана». Прим.: кодер взял `StackFileVersion=0` (не `=N`) — это ВЕРНЕЕ: ровно faithful post-crash-состояние fresh-пути, и 0→1 transition сам заассерчен (сильнее, чем `=N`). Ветка выбирается существованием v-dir, не версией → :96-98 покрыт. - **F2 (незаклэмпленное удаление базы) — ЗАКРЫТО.** `migrate_2_44_0.go:168`: удаление теперь `filesystem.JoinPaths(basePath, fileName)` вместо `filepath.Join` — консистентно с заклэмпленными read (`GetFileContent`) и write (`StoreStackFileFromBytesByVersion`) в ЭТОЙ ЖЕ функции. `JoinPaths` клэмпит (`filepath.Join(root, filepath.Join("/", untrusted))` → `../` схлопывается против `/` до join с root). Для нормального fileName путь **байт-идентичен** старому (basePath trusted root) → нет изменения поведения / пропущенных удалений; traversal через `../` в EntryPoint/AdditionalFiles больше не выходит за basePath в file-DELETING пути. Импорты чисты (`filesystem` используется :168, `path/filepath` ещё нужен в `stackVersionDirComplete` :182 → нет unused-import build-break). Регрессий нет — наоборот, закрыта пред-существовавшая рассогласованность read(clamped)/delete(unclamped). ### Do — apply these, then re-review _(нет)_ --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора) - `[style/linter]` `info` **[conventions]** `migrate_2_44_0_test.go:403` строит проверку через `filesystem.JoinPaths(basePath, "docker-compose.yml")`, тогда как соседние тесты — конкатенацией `basePath + "/..."`; чище, но единственная стилевая рассогласованность. Косметика. - `[below-threshold]` `low` **[stability, round-1 перенос]** `stackVersionDirComplete` проверяет только существование, не контент/размер: усечённый мид-`io.Copy` крэшем `v{N}/file` пройдёт как complete → adopt. База выживает (adopt её не удаляет) → нет перманентной потери; совпадает с не-атомарной нормой кодовой базы. Опц.-харден. - `[speculative]` `low` **[stability, round-1 перенос]** no-fsync power-loss между записью v{N} и удалением базы — пред-существующее, process-crash восстановим. --- ### Вне scope — кандидаты на отдельную задачу (NOT part of this verdict) - **[debt, round-1 перенос] one-shot heal не перезапустится при дрейфе ПОСЛЕ 2.44.0** — `migrate_2_44_0.go` — миграция version-gated; будущий restore pre-2.44 бэкапа → снова 500 без авто-починки. Постоянно закрыл бы idempotent repair-on-startup. Отдельный трек (принятый дизайн version-pinned миграции, не форк). - **[cleanup, round-1 перенос] incomplete pre-existing `v{N}` (v>0) НЕ лечится** (skip навсегда) — узкий вариант #30 остаётся с 500; data-safe, консервативный scope; лечить = write-risk. Отдельный трек. <!-- state:review reviewed_head=7b5dfeac3fbf2132444c18a766ce956d1a6cff96 round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-03 01:32:43 +03:00
vvzvlad merged commit 7d71e9e788 into develop 2026-07-03 01:33:37 +03:00
Sign in to join this conversation.