Repository navigation
スプリント m8a - #462
スプリント m8a#462
Conversation
zsh/zpty で対話の zsh を起こし compadd を包んで、[name] と --context の 集合のすべての道で etc/_devbase が出す候補を期待と比べる。zsh か zsh/zpty が 無ければ、DEVBASE_TEST_REQUIRE_ZSH=1 のときだけ落とし、ほかは飛ばす。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zsh を起動する検査が無いことを前提にした記述(決定 9・決定 13・テスト観点)を改め、 補完の実行テストの小節と決定 14〜17 を足す。CI の確定仕様に pytest のジョブの zsh と印を足す。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#455 を実装する(スプリント m8a)
除外の一覧を exclude.py で組み立て、フル・差分・復元前バックアップの tar に --anchored --no-wildcards-match-slash でメンバーより前に渡す。世代の meta.yml に 作ったときの除外の一覧を残し、違う一覧の世代へは差分を積まない(自動は新しい世代、 名前の指定は止める)。設定が読めない間は世代の記録の一覧で控える。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#368 を実装する(スプリント m8a)
#368 の受け入れ条件 9 の確認(conductor、2026-10-12)合格。 既定の除外のパスを消したホームで dev コンテナを起動し、4 つの CLI はどれもエラーで止まらずに起動した。
知っておくこと: 復元の直後、Claude Code の最初のセッションが始まるまでは、 |
改修計画 — devbasex/devbase #462
改善項目I-001 —
|
| 兆候 | 手法 | 重要度 | 等級 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|---|
| deep_nesting | extract_method | minor | high | claude / kiro | 採用 | 1 |
なぜ: 設定を読めないときの 3 段のフォールバック(作り直す世代の記録 → 系列の最新の世代の記録 → 今の一覧)のうち、2 段目が series_latest・_safe_snap_dir の例外の握り・is_dir・_recorded_excludes を 3 段の入れ子で同居させ、関数の主筋(どの順で記録を選ぶか)が読みにくい。認知的複雑度 11 は本体の上位 5 番目
手順: 1. 2 段目を _latest_recorded_excludes(self, volumes) -> Optional[tuple] として抽出する(series_latest が None・_safe_snap_dir が SnapshotError・ディレクトリでない・記録が無い、のいずれでも None を返す)
2. _excludes_for を「読めたら今の一覧 → 作り直す世代の記録 → _latest_recorded_excludes → 今の一覧」の平らな順に書き換える(return self._recorded_excludes(snap_dir) or ... にはせず、空のタプルの記録を落とさないよう is not None の判定を残す)
3. tests/snapshot/test_exclude_generations.py の unreadable_config 系(limit 後の full・snar の fallback・pre-restore・記録無し)を流して振る舞いが変わらないことを確かめる
I-002 — lib/devbase/snapshot/manager.py#SnapshotManager.auto_snapshot_target
| 兆候 | 手法 | 重要度 | 等級 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|---|
| duplication | extract_method | major | high | kiro | 採用 | 1 |
なぜ: 差分を積める世代かの判定(対象ボリュームの一致・archive_root が members・除外の一覧が今と同じ)が auto_snapshot_target と _create_incremental の 2 か所に同じ条件で書かれている。auto_snapshot_target は 3 条件を INFO ログ + None で、_create_incremental は同じ 3 条件を SnapshotError で表す。片方だけ条件を直すと「積み先に選ぶが、積もうとすると拒否される」食い違いが起きる。auto_snapshot_target は認知的複雑度 10・52 行・return 7 本。
手順: 1. 3 条件のうち失敗した 1 件を表す結果を返す純粋な判定を 1 つ抽出する(例: _incremental_block_reason(snap_dir, volumes) が「不一致」「源が古い」「除外が違う」「積める」の区別を返す。副作用なし)
2. auto_snapshot_target は抽出した判定の戻り値で分岐し、理由ごとの INFO ログと None を出す(ログ文言と戻り値は現状維持)
3. _create_incremental は同じ判定の戻り値で分岐し、理由ごとの SnapshotError を送出する(例外メッセージは現状維持)
4. tests/snapshot/test_auto_snapshot_series.py と test_exclude_generations.py を実行し、ログ文言・例外 match・積み先の挙動が不変であることを確認する
I-003 — lib/devbase/snapshot/exclude.py#_load_user_excludes
| 兆候 | 手法 | 重要度 | 等級 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|---|
| long_method | extract_method | minor | medium | claude / kiro | 採用 | 1 |
なぜ: 1 関数に「ファイルの存在確認」「読み込みと YAML 解釈」「最上位が辞書か・exclude がリストかの構造検証」「各パターンの正規化と警告」が同居する。認知的複雑度 9・CC 9・8 分岐・22 文。構造検証と正規化が同じ try に混ざり、どの段で失敗したかが読みにくい。
手順: 1. try の中身(open → safe_load → None の補い → 辞書/リストの検証)を _read_exclude_values(path) -> list として抽出し、形が違えば ValueError を投げる形はそのまま保つ
2. _load_user_excludes は「無ければ ([], True) → _read_exclude_values を同じ例外の組で受けて警告し ([], False) → 値ごとに normalize」の 3 段にする
3. tests/snapshot/test_exclude.py(壊れた YAML・最上位が辞書でない・exclude がリストでない・個別の値の警告)を流す
I-004 — lib/devbase/snapshot/exclude.py#resolve_excludes
| 兆候 | 手法 | 重要度 | 等級 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|---|
| duplication | consolidate_duplication | minor | medium | claude | 採用 | 1 |
なぜ: 除外の一覧を比べられる形(重複を除いて並べ替える)にする知識が exclude.py の resolve_excludes の tuple(sorted(set(...))) と manager.py の _recorded_excludes の tuple(sorted(set(value))) に 2 か所あり、さらに _excludes_differ が set へ戻して比べ、_create_full が sorted(excludes) で記録する。正規の形の定義が分散していて、片方だけ変えると記録と今の一覧の比較が食い違う
手順: 1. exclude.py に canonical(patterns) -> tuple(重複を除いて並べ替えたタプル)を置く
2. resolve_excludes と SnapshotManager._recorded_excludes をこの関数で書き換える
3. 両方が正規の形になったので、_excludes_differ の比較を recorded != self.excludes にする(set への変換を外す)
4. tests/snapshot/test_exclude.py と test_exclude_generations.py を流す
I-005 — lib/devbase/snapshot/manager.py#SnapshotManager.excludes
| 兆候 | 手法 | 重要度 | 等級 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|---|
| data_clump | inline | minor | medium | claude | 採用 | 1 |
なぜ: _excludes と _exclude_config_readable の 2 つの属性が常に組で作られ、遅延の初期化を _resolve_excludes と 2 つのプロパティが手で分担している(None を未計算の印に使う隠れた状態)。resolve_excludes の戻り値の組を functools.cached_property 1 つに持てば、初期化の印と 3 つのメソッドが要らなくなる
手順: 1. @cached_property def _exclude_resolution(self) -> tuple: return resolve_excludes(self.backups_dir) を置く(警告を 1 実行 1 回にする理由のコメントを移す)
2. excludes と exclude_config_readable はそれぞれ self._exclude_resolution[0] / [1] を返すだけにする
3. init の _excludes・_exclude_config_readable と _resolve_excludes を消す
4. test_warnings_are_emitted_once_per_manager を含む tests/snapshot を流す
I-006 — lib/devbase/snapshot/manager.py#SnapshotManager._create_incremental
| 兆候 | 手法 | 重要度 | 等級 | 提案元 | 状態 | コミット |
|---|---|---|---|---|---|---|
| duplication | consolidate_duplication | major | low | codex | 採用 | 1 |
なぜ: auto_snapshot_target と _create_incremental が、世代のボリューム構成 → archive_root → 除外一覧の同じ互換性判定を別々に持つ。除外の条件を変える際に両方を追う必要があり、自動選択と名前指定で差分を積める条件がずれる余地がある。ログで新世代へ進むか例外で停止するかは別の責務なので残す。
手順: 1. test_manager_volumes.py・test_auto_snapshot_series.py・test_exclude_generations.py の既存ケースを確認し、複数の不一致が同時にある場合の判定優先順位を公開 API 経由の現状固定テストで補う。
2. manager.py 内に、現行と同じ順番・遅延評価で最初の不一致と必要な記録済みボリュームを返す非公開ヘルパーを抽出する。判定のためのメタデータ読み込み順や例外伝播を変えない。
3. 2 つの呼び手を共通判定へ置き換え、それぞれのログ・例外の文言を保つ。自動選択だけのディレクトリ存在条件、差分数上限、snar 不在時のフォールバックは呼び手に残す。
4. snapshot 関連テストで返り値・警告・失敗時のファイル不変を確認する。
レビューで確かめるテストの差分: tests/snapshot/test_exclude_generations.py(機械で期待値が変わったか決まらなかった)
指標の測定
- 状態: written / 言語とツールの対応: 既定(.ndf/code-metrics.json は無い) / 所要: 10.0 秒(使えた時間 90 秒 / 上限 90 秒)
- 指標のファイル: /var/folders/qz/qxt0p_y15xv5jg4x77zmtxj80000gp/T/ndf-worktrees/devbasex--devbase/rf462/work/.cross_refactoring/code-metrics-rf462.md
| 言語 | ツール | 版 | ランナー | ファイル | 秒 | 結果 |
|---|---|---|---|---|---|---|
| python | ruff-complexipy | ruff 0.16.9 / complexipy 8.0.1 | uvx | 203 | 2.8 | 測った |
| 重複: python | symilar | pylint 4.0.9 | uvx | 203 | 6.8 | 測った(3 箇所) |
時間の上限
| 値 | 中身 |
|---|---|
| 提案の枠の終わり(監視の上限 = 残り + 余裕) | 2026-10-12T20:26:58+09:00 |
| 改修計画の枠の終わり(同上) | 2026-10-12T20:29:58+09:00 |
| テストの追加の終わり = 実装の終わり − Σ(採っていて未検証の項目の実装の見積り) | 2026-10-12T20:27:20+09:00 |
| 実装の終わり = 最終ゲートの修正の打ち切り − バッファ − Σ(採っていて未検証の項目の検証の見積り) | 2026-10-12T20:32:32+09:00 |
| 直しの試行の打ち切り | 2026-10-12T20:41:11+09:00 |
| 最終ゲートの修正の打ち切り(想定最大時間の終わり) | 2026-10-12T20:48:50+09:00 |
| 打ち切りの後の取り消し(案 A)の締め切り = 最終ゲートの修正の打ち切り + 0.20·B(工程の 1 つとして起動したとき) | 2026-10-12T20:54:50+09:00 |
| 最終ゲートの修正の 1 回目に必ず渡す長さ(秒。予備時間の final_fix) | 330 |
| 指標の測定の上限(秒。提案の枠の中から割く) | 90 |
| 着手前のテスト 1 回の上限(秒) | 375 |
| 範囲テスト 1 回の上限(秒) | 387 |
| 手元の全体テスト 1 回の上限(秒) | 375 |
| CI の待ちの上限(秒) | 243 |
| 余裕(秒。手順の上限と CLI の上限に足す) | 90 |
入力: 予算 30 分 / 全体テストの所要 w 124.8(ndf-record) / 着手前の実測 x 128.8 / CI の壁時計 c 81.0 / 範囲テストの所要 s None(—) / 戦略 local-full
無音の打ち切りは手順の監視の上限と同じ値である。項目ごとの期限は持たない(実装を止めるのは実装の終わりだけ)。
見送った提案
| 対象 | 兆候 | 理由 | 根拠(MVV) | 補足 |
|---|---|---|---|---|
tests/snapshot/test_exclude_generations.py#_broken_config |
duplication | budget | Vision | 見積り 1.4 分が残りに入らない |
tests/snapshot/test_manager_series.py#write_state |
divergent_change | budget | Vision / Value 3 | 見積り 1.4 分が残りに入らない |
tests/cli/zsh_completion_harness.py#run_completions |
long_method | budget | Vision / Mission | 見積り 1.6 分が残りに入らない |
lib/devbase/snapshot/manager.py#SnapshotManager._apply_rotation_plan |
long_method | budget | Value 2 | 見積り 1.6 分が残りに入らない |
lib/devbase/snapshot/manager.py#SnapshotManager._warn_about_lost_renames |
long_method | budget | Value 2 | 見積り 1.6 分が残りに入らない |
lib/devbase/snapshot/exclude.py#tar_exclude_args |
primitive_obsession | budget | Vision | 見積り 1.3 分が残りに入らない |
lib/devbase/snapshot/manager.py#SnapshotManager._extract_archive |
data_clump | budget | Value 2 / Value 3 | 見積り 1.3 分が残りに入らない |
tests/containers/test_base_dockerfile_fonts.py#_run_blocks |
duplication | budget | Vision | 見積り 1.4 分が残りに入らない |
tests/containers/test_base_image_font_matching.py#_docker_unavailable |
duplication | budget | Vision | 見積り 1.4 分が残りに入らない |
tests/containers/test_entrypoint_ai_settings_concurrent.py#run_concurrent |
divergent_change | budget | Vision / Value 2 | 見積り 1.4 分が残りに入らない |
tests/snapshot/test_exclude_generations.py#RecordingManager |
duplication | budget | Vision | 見積り 1.4 分が残りに入らない |
lib/devbase/snapshot/manager.py#SnapshotManager._load_metadata |
duplication | budget | Value 2 | 見積り 1.6 分が残りに入らない |
lib/devbase/snapshot/manager.py#SnapshotManager.list |
divergent_change | budget | Value 2 | 見積り 1.6 分が残りに入らない |
tests/containers/test_base_shellcheck_ci.py#_shellcheck_commands |
deep_nesting | budget | Vision | 見積り 1.6 分が残りに入らない |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | APPROVE
指定コミットを静的レビューし、修正を要する critical / major の指摘はありません(判断の根拠: Vision・Value 2・Value 3)。テストは指示に従い未実行。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | kiro | APPROVE
スナップショットの除外機能 (#368) をレビューした。除外一覧の組み立て・検査・tar 引数のアンカー、世代ごとに同じ除外一覧で控える制約 (auto_snapshot_target 順 4a / _create_incremental のガード)、読めない設定での世代記録の継承 (読めない設定を理由に新しい世代を作ってローテーションで古い世代を消さない) は、仕様 (snapshot-series.md 順 4a・データ/設定表) と実装が一致している。_validate_volumes のボリューム制限は不変で、復元前の消去が devbase 以外のボリュームへ及ぶ経路は増えていない。tests/snapshot/test_backup_real_tar.py が実 GNU tar で除外の効き目を縛り、test_exclude_generations.py が読めない設定の各経路 (差分上限の新フル・snar フォールバック・復元前バックアップ・記録なし) を網羅する。test_restore_incremental.py の入れ替え場所を除外外 (data/cache) へ移した変更も、除外内だと検証が無効化される問題を正しく避けている。critical / major の基準 (利用者経路の誤動作・データ破壊・秘密/認可/戻せない操作・設計と実装の食い違い) に当たる指摘は見つからなかった。
🔧 /ndf:fix サマリ | commit (なし)対応件数: critical=0 / major=0 / minor=0(合計 0 件) |
#455 のマージ前の手動確認(conductor、2026-10-12、
|
印 DEVBASE_TEST_REQUIRE_ZSH |
結果 |
|---|---|
| 無し | 8 passed, 9 skipped。飛ばした理由は zsh が無いため、zsh の補完の実行テストを飛ばす。-q でも warnings summary が出る。ほかのテストは走る |
1 |
8 passed, 1 skipped, 8 errors。理由は DEVBASE_TEST_REQUIRE_ZSH=1 なのに zsh が無い。zsh の補完の実行テストを流せない |
#368 の受け入れ条件 1〜4・8 の確認(conductor、2026-10-12、
|
| 受け入れ条件 | 操作 | 結果 |
|---|---|---|
| 1・2 | フル(full.tar.zst、56 エントリ) |
除外の 9 つは 1 つも無い。外さないもの 9 つはすべてある(深い場所の同じ名前も残る) |
| 14 | meta.yml の exclude |
既定の除外の 10 項目が並べ替えて残る |
| 3 | .codex/logs_2.sqlite と .codex/sessions/x を書き換えて差分(incr-001.tar.zst) |
書き換えた logs_2.sqlite は無い。書き換えた sessions/x はある |
| 8 | 両ボリュームにファイルを足してから復元 | 除外したパスは無い。足したファイルは消えた。.codex/sessions/x は差分の時点の中身(v2) |
| 4 | 復元前バックアップ(pre-restore-* の full.tar.zst、58 エントリ) |
除外の 9 つは無い |
後片付け: 作業用のボリューム 2 つと一時のルートを消した。
…nager._excludes_for 設定を読めないときの 2 段目 (系列の最新の世代の記録) を _latest_recorded_excludes へ抽出し、_excludes_for を 今の一覧 → 作り直す世代の記録 → 最新の世代の記録 → 今の一覧 の平らな順にした。 空のタプルの記録を落とさないよう is not None の判定は残す。 Item-Id: I-001 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nager.auto_snapshot_target 差分を積める世代かの 3 条件 (対象ボリューム・控えの起点・除外の一覧) を _incremental_block_reason へ抽出した。auto_snapshot_target は理由ごとに INFO と None、_create_incremental は理由ごとに SnapshotError を出す。 調べる順・メタデータの読み方・ログと例外の文言は変えない。 Item-Id: I-002 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_excludes 読み込み・YAML の解釈・形の検証を _read_exclude_values へ抽出した。 _load_user_excludes は「無ければ読めたとみなす → 読めなければ警告して空 → 値ごとに正規化」の 3 段になる。受ける例外の組と警告の文言は変えない。 Item-Id: I-003 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…esolve_excludes 除外の一覧を比べられる形 (重複を除いて並べ替えたタプル) にする知識を exclude.canonical の 1 か所に寄せ、resolve_excludes と SnapshotManager._recorded_excludes から使う。両側が同じ形になったので _excludes_differ は set へ戻さずタプルのまま比べる。 Item-Id: I-004 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cludes 常に組で作られる _excludes・_exclude_config_readable と、None を未計算の印に 使う _resolve_excludes を、resolve_excludes の戻り値を持つ cached_property _exclude_resolution 1 つにまとめた。excludes・exclude_config_readable は その組の要素を返すだけになる。警告は今までどおり 1 つのマネージャーで 1 回。 Item-Id: I-005 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…napshotManager._create_incremental 自動の積み先と名前を指定した差分が同じ判定 (_incremental_block_reason、I-002 で 抽出済み) を通ることを縛る現状固定テストを足した。積めない理由が重なるときは 対象ボリューム → 控えの起点 → 除外の一覧の順に最初の 1 件だけを理由にする。 足したテストはリファクタリング前のコード (1bed59f) でも通る。 Item-Id: I-006 Impl-Runtime: claude Impl-Model: default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | kiro | APPROVE
rf462 の 6 件のリファクタリング (manager.py / exclude.py) を round 2 の観点でレビューした。合流元 1bed59f からの新規差分は exclude.py・manager.py・test_exclude_generations.py の 3 ファイルのみで、docs の変更は既にマージ済みの #460/#461 由来のため本 PR の新規範囲ではない。canonical() は旧 tuple(sorted(set(...))) と等価、_incremental_block_reason は 2 呼び出し元の短絡順序と SnapshotError の伝播を保存、_latest_recorded_excludes は旧インライン実装と同一、cached_property は『manager あたり警告 1 回』を保存、_excludes_differ の tuple 比較は両辺が canonical なので妥当。削除した _resolve_excludes/_excludes/_exclude_config_readable への残参照なし・slots 衝突なし。新テストが積めない理由の優先順位を 2 経路で現状固定している。critical / major の基準に当たる指摘は無し。
リリース後テスト(#368、conductor、2026-10-12、main
|
| 確認 | 結果 |
|---|---|
| 新しいフルの世代に既定の除外のパスが入らない | 合格。 full.tar.zst の一覧で既定の除外 10 項目に当たるエントリは 0 件 |
meta.yml に除外の一覧が残る(受け入れ条件 14) |
合格。 exclude: に 10 項目が並ぶ |
| フルの控えの対象が除外の分だけ減る(性能の要求) | 合格(見込みより小さい)。 今のボリュームで除外のパスの合計は 1,265MB(共通 1,262MB・グループ 3MB)で、これが控えの対象から外れた。要求の見込み(約 1.9GB、2026-10-02 の測定)より小さいのは、.codex/logs_2.sqlite などの大きさが測った時点と違うため |
前の世代との比較(同じ系列 devbase_home_ubuntu + devbase_home_team-a):
| 世代 | 作成 | full.tar.zst |
展開後の合計(tar の一覧の大きさの和) | うち除外の対象 |
|---|---|---|---|---|
| 20261002-191730(変更前) | 2026-10-02 | 3.3GB | 16,955MB | 1,440MB(73,682 件) |
| 20261012-211605(変更後) | 2026-10-12 | 3.7GB | 17,810MB | 0 |
新しいフルのほうが大きいのは、10 日でボリュームそのものが伸びたため(除外を除いた分で 15,515MB → 17,810MB)。除外が効いていなければ、今のボリュームのフルの展開後は約 19,075MB になっていた。
作った世代 20261012-211605 は残した(利用者の判断)。
振り返り(2026-10-12)対象: issue #455 / issue #368 / PR #462(実装 #460・#461、確定仕様化 #464) #455 は zsh を 何が起きたか
context window の大きさこの会話(仕上げ)の conductor の記録だけを測った。スプリントの前半の会話は含まない。
目印の付いた行は無い。 承認ゲートの差し戻し
MVV を見直すか
次に変えること
途中で起票した課題
|
スプリント m8a の課題を main へ取り込む。
利用者向けの変化
backups/exclude.ymlに書いたパスも控えから外れます。(#368 を実装する(スプリント m8a) #461)移行の手順
課題と設計
コミット
変更の統計
テスト(supervise.py の run のステップ)
集めた実装の PR
閉じる課題
Closes #368
Closes #455
手動確認
etc/_devbaseをgit show 54ed5bf^:etc/_devbaseの内容に一時に差し替えてテストを流し、落ちることと出力を確かめて戻す。結果を実装の PR に貼るPATHから外した環境(またはzsh/zptyを読めない形)でテストを流し、出力を実装の PR に貼るdevbase-snapshotイメージで作成・差分・復元を行い、アーカイブの中身の一覧と復元後のボリュームを見る。受け入れ条件 9: 使い捨てのホームのボリュームから既定の除外のパスを消して dev コンテナを起動し、4 つの CLI を起動するdevbase snapshot create --fullの大きさを変更前の世代と比べる(リリース後テスト)モード: standard / 通した工程: 実装 → Pull Request
🤖 Generated with Claude Code