refactor(PLAN66): スナップショットの名前の検証を utils/names の述語へ寄せる (#203) - #235
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_VALID_NAME_RE を消し、_validate_name が is_single_segment_name を呼ぶ(決定 5)。 例外の型・文言・_safe_snap_dir の封じ込めは変えない。狭まるのは末尾の改行を持つ名前だけ。 tests/snapshot/test_manager_name.py で受理 4 件・拒否 8 件を固定する(決定 6)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix current public-entry behavior before directory creation or Docker execution. Item-Id: R1-001 Round: 1 Impl-Runtime: codex Impl-Model: default
改修計画 — devbasex/devbase #235
ラウンド 1(実装 codex / レビュー agy / kiro)R1-001 —
|
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | kiro | 採用 | 1 |
なぜ: create は最初に _safe_snap_dir(name) -> _validate_name を通すが、不正な名前が公開入口 create から拒否されることは固定されていない。既存テストは private な _validate_name を直接呼ぶだけで、公開境界での拒否 (mkdir/Docker へ進む前に SnapshotError) を固定していない。
手順: 1. SnapshotManager(tmp_path) を作る
2. is_single_segment_name が弾く名前 (例 '../evil') を create(name=...) に渡す
3. SnapshotError が上がり、backups/ 配下にディレクトリが作られていない (Docker へ進んでいない) ことを比較する
R1-002 — lib/devbase/snapshot/manager.py#SnapshotManager.delete
| 兆候・経路 | 手法・階層 | 重要度 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|
| error | unit | — | kiro | 検証中 | 1 |
なぜ: delete も先頭で _safe_snap_dir(name) -> _validate_name を通すが、不正な名前を公開入口 delete から拒否することは固定されていない。存在しない名前の 'not found' 経路とは別に、名前が不正な段階で shutil.rmtree へ進まず止まることを公開境界で固定できる。
手順: 1. SnapshotManager(tmp_path) を作る
2. is_single_segment_name が弾く名前 (例 '_foo') を delete(name=...) に渡す
3. SnapshotError が上がることを比較する (rmtree へ進まないので backups/ 配下は変化しない)
見送った項目
(なし)
Fix current public-entry behavior before directory deletion or rmtree execution. Item-Id: R1-002 Round: 1 Impl-Runtime: agy Impl-Model: default
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | kiro | APPROVE
_VALID_NAME_RE を utils/names.is_single_segment_name へ寄せる変更は妥当。文字集合が同一で re.match→fullmatch により末尾改行のみ拒否が狭まる点は CHANGELOG と仕様書に正確に反映され、受理4件・拒否8件と公開入口(create/delete)の副作用抑止テストで固定されている(uv run --locked pytest tests/snapshot/test_manager_name.py → 14 passed を確認)。path-traversal 防止はむしろ強化される方向で、正確性・セキュリティ・保守性の観点で修正を要する箇所は見当たらない。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | agy | APPROVE
スナップショット名の検証共通化(PLAN66)、受理・拒否境界および副作用抑止のテスト、仕様書・CHANGELOG の整合性を確認しました。修正を要する指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | APPROVE
修正を要する新規指摘はありません。関連テスト 107 件が成功しました。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | kiro | APPROVE
スナップショット名検証を utils/names.is_single_segment_name へ寄せる焦点の合った変更で、修正提案なし。旧 ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ (.match) → 新 fullmatch([A-Za-z0-9][A-Za-z0-9._-]*) の差は末尾改行の拒否のみで、文字集合は同一。受理/拒否と公開入口の副作用非到達がテストで固定され (tests/snapshot/test_manager_name.py)、re import は他正規表現で継続利用。CHANGELOG / 仕様の記述も変更範囲と一致。
Summary
設計文書
issues/PLAN66_project-name-validation-design.mdの「実装の分け方」の 2 本目(スナップショットの寄せ。F3・決定 5・6)。計画は
issues/PLAN66_project-name-validation-impl2.md。参照:
release/v3.7.0)Closes #203
変更点
docs(PLAN66): 実装 2 本目の計画を置いたrefactor(PLAN66):lib/devbase/snapshot/manager.pyの_VALID_NAME_REを消し、_validate_nameがutils/names.is_single_segment_nameを呼ぶ(決定 5)SnapshotError)・文言(「英数字・ハイフン・アンダースコア・ドットのみ使用可能、先頭は英数字」)・_safe_snap_dirの封じ込め(resolve()+startswith)は変えていないnot nameのガードは述語が空を弾くため外した(呼び出し元はいずれも文字列を渡す。createはNoneを時刻の名前へ置き換えてから呼ぶ)abc\n)だけ。文字集合は同じ([A-Za-z0-9][A-Za-z0-9._-]*)で、re.match+$がfullmatchに変わった差だけtests/snapshot/test_manager_name.py(新設):_validate_nameの受理 4 件(ok-name・a.b・A_b・0abc)と拒否 8 件(_foo・.x・-x・空・..・café・a/b・abc\n)を固定(決定 6。受け入れ条件 11 の列挙と同じ集合)。拒否は文言無効なスナップショット名も見るdocs(PLAN66): 確定仕様docs/specifications/cli-argument-resolution.mdの「運用」の 2 つ目の箇条書きだけを書き換え(寄せていないのはenv/bundle.pyとenv/secret_store.pyの 2 つ、スナップショットは述語を共有、その理由と固定テストの在り処)。CHANGELOG.mdの[Unreleased]に### Changedを足した(1 本目の Added の行は変えていない)test(構造改善で足した 2 件。9c65ae1/e32c639): 公開の入口(create/delete)から不正な名前が拒否され、subprocess.run/shutil.rmtreeへ進まないことを固定した(tests/snapshot/test_manager_name.py)採らなかった案(文言を
NAME_FORM_HINTへ寄せる・逆向きに寄せる)は実装していない。空の名前の検査を外したことの確認(決定 5 との整合)
決定 5 は「例外の型・文言・
_safe_snap_dirの封じ込めを変えない」と決めている。外したnot nameのガードが利用者向けの文言を変えていないことを確かめた。変更前(
1111926:lib/devbase/snapshot/manager.py)の_validate_nameはif not name or not _VALID_NAME_RE.match(name):という 1 つのifで、空の名前も正規表現に外れた名前も 同じraise・同じ文言(無効なスナップショット名: '<名前>' (英数字・…))を出していた。空の名前だけの別の文言は存在しない変更後も空の名前は述語(
fullmatch)が弾き、同じ文言が出る。隔離したDEVBASE_ROOT/HOMEでの実行:したがって削除を維持する(文言が同じであることを確かめたため)。戻すと同じ文言を出す分岐が 2 つになり、述語と重なる
受け付ける名前が狭まるのが末尾の改行 1 ケースだけであることも、隔離した環境で確かめた(下の Test plan)。
Test plan
release/v3.7.0を base にした PR では CI が動かないため(#216)、手元で実行した。CI が行う検査(compileall/ruff)も手元で実行した。abc\nの 1 件だけが落ちることを確かめたuv run --locked pytest tests/snapshot/test_manager_name.py -q→1 failed, 11 passed(FAILED ...test_validate_name_rejects[abc\n]/DID NOT RAISE)uv run --locked pytest tests/snapshot/ -q→ exit=0、87 passed in 5.00s(2026-09-23 02:49)grep -n "_VALID_NAME_RE" lib/devbase/snapshot/manager.py→ 0 件(exit=1)uv run --locked pytest tests/ -q→ exit=0、2936 passed in 143.10s(2026-09-23 01:30。heade32c639)uv run --locked python -m compileall -q lib bin→ exit=0 /uvx ruff check --select=E9,F63,F7,F82 lib→ exit=0(All checks passed!)docs/specifications/cli-argument-resolution.md・CHANGELOG.md)DEVBASE_ROOTとHOME。本番の系には触れていない)./bin/devbase snapshot create --name '_foo'→ exit=1、無効なスナップショット名: '_foo' (…)./bin/devbase snapshot create --name $'abc\n'→ exit=1、同じ文言(この 1 ケースだけが新たに弾かれる)./bin/devbase snapshot delete _foo→ exit=1、同じ文言./bin/devbase snapshot create --name ''→ exit=1、同じ文言(上の節)DEVBASE_ROOTの下にはディレクトリが作られない(findで確認)検査の工程
/ndf:cross-refactoring。範囲はsnapshot/manager.pyとtests/snapshot/test_manager_name.py、--severity-threshold major): テスト整備で 2 件を採用・適用(上の変更点 5)。構造改善の提案は 5 件とも PR の差分(_validate_nameとそのテスト)の外(restoreの分割・アーカイブ名の定数化など)を指したため採らなかった。最終ゲートは passed/ndf:cross-review): 収束(approved)。round 1 = agy + kiro、round 2 = codex + kiro。3 者とも同じ head(e32c639)を見て指摘 0 件。未解決スレッド 0 件(GitHub 側で確認)🤖 Generated with Claude Code