Skip to content

feat: PLAN32-config-loader project.yml ローダと wire format - #104

Merged
takemi-ohama merged 7 commits into
release/PLAN32from
feature/PLAN32-config-loader
Aug 22, 2026
Merged

feat: PLAN32-config-loader project.yml ローダと wire format#104
takemi-ohama merged 7 commits into
release/PLAN32from
feature/PLAN32-config-loader

Conversation

@takemi-ohama

@takemi-ohamatakemi-ohama commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

PLAN32 の土台となる プロジェクト設定 projects/<name>/project.yml のローダを追加します。

  • lib/devbase/project/config.py
    • load_project_config(project_dir) / parse_project_config(data, source): YAML を読み、defaults の継承・既定値の補完・検証を経て正規化した ProjectConfig / RepoSpec を返す
    • encode_repo_plan() / decode_repo_plan(): コンテナへ渡す clone プランの wire format(base64 / US 区切り)。entrypoint との契約をこの PR で確定させる
  • 呼び出し元はまだ差し替えていないため、この PR 単体では devbase の挙動は変わりません

設計上の判断

  • 未知キーはエラーにする(brunch: main のような typo が「設定したのに効かない」形で表れないように)

  • project.yml が無い場合も旧 envGIT_USER/GIT_REPO)へフォールバックしない。PLAN32 は後方互換を持たない方針であり、移行漏れを黙って動かすと検出できないため、移行コマンドを案内するエラーにする

  • clone プランは base64 の US (\x1f) 区切りテキスト(1 行 1 repo、末尾 LF あり)。区切りにタブを使うと bash の既定 IFS と同じ空白類のため空フィールドが畳まれ、branch 未指定の行で init がずれる。entrypoint 側が base64 -dwhile read だけで解釈でき、コンテナイメージに YAML/JSON パーサ依存を増やさない。base64 化により compose の変数展開($)や改行で構成ファイルが壊れることも防ぐ

  • dir/work 直下の 1 階層に限定し、空白・/ の混入を弾く(clone 先が /work の外へ抜けない、wire format が壊れない)

  • plan: issues/PLAN32_multi-repo-project.md の Task 1

  • release PR: release: PLAN32 1 プロジェクト = 複数リポジトリ構成への変更 (v3.0.0) #103

Test plan

  • uv run pytest tests/project/ — 55 件(正常系: 既定値 / defaults 継承 / host 混在 / primary 明示、異常系: 必須欠落 / 型不一致 (数値のみの repo 名) / dir 重複 / primary 複数 / 未知キー / version 不一致 / dir の階層逸脱 / 空白混入 / 壊れた YAML、version が非整数 (真偽値 / float / 文字列) / defaults が非マッピング / 非 UTF-8 保存、wire format: 符号化結果・往復・末尾 LF・base64 の健全性・init 列の値検証、実 bash consumer での契約テスト: IFS=$'\x1f' read で branch 未指定行の init がずれないこと / 1 repo 構成で最後の行が落ちないこと)
  • uv run pytest — 全 1299 件 green(既存への影響なし)

PLAN32 Task 1。projects/<name>/project.yml を読み、defaults 継承・既定値
補完・検証を経て正規化した ProjectConfig を返すライブラリを追加した。
コンテナへは base64 TSV の clone プランとして渡す契約 (encode/decode) も
ここで確定させる。呼び出し元の差し替えは後続 PR で行うため、この PR 単体
では挙動は変わらない。
未知キー・dir 重複・primary 複数・空白混入は黙って通さずエラーにする。
project.yml が無い場合も旧 env 形式へフォールバックせず、移行コマンドを
案内するエラーにした (PLAN32 は後方互換を持たないため、移行漏れを検出
できる必要がある)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | codex | REQUEST_CHANGES

YAML の型検証で無効値を受理する 2 箇所を修正してください。

Comment threadlib/devbase/project/config.py Outdated
Comment threadlib/devbase/project/config.py

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 1 | gemini | REQUEST_CHANGES

ファイル読み書きの際にプラットフォーム依存のエンコーディング (Windows の CP932 など) が使われる箇所があります。UTF-8 を明示的に指定して動作を安定させてください。

Comment threadlib/devbase/project/config.py Outdated
Comment threadtests/project/test_config.py Outdated
- version: YAML の `true` が Python では `1` と等価なため bool を明示的に拒否
- defaults: `or {}` をやめ、null/未指定のみ空マッピングへ正規化
(`defaults: []` / `false` が黙って受理され型検証が素通りするのを防ぐ)
- project.yml の読み込みに encoding="utf-8" を明示し、UnicodeDecodeError は
「UTF-8 で保存してください」と案内する ConfigError に変換
- テストの write_text にも encoding="utf-8" を明示 + 上記の回帰テストを追加
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@takemi-ohama

Copy link
Copy Markdown
ContributorAuthor

🔧 /ndf:fix サマリ (round 1)

対応件数: critical=0 / major=1 / minor=3 (合計 4 件)
deferred: 0 件 / rejected: 0 件
commit: 7a33c33
CI: NONE (このブランチに紐づく check は未設定)

uv run pytest 全 1286 件 green (tests/project/ は 34 → 42 件に増加)。

詳細

#指摘元箇所再判定対応
1codexlib/devbase/project/config.py version 検証minorisinstance(version, bool) を明示的に拒否。version: trueTrue == 1 で schema version 1 として受理されていた
2codexlib/devbase/project/config.py defaults 正規化minoror {} を廃止し None のみ {} へ。defaults: [] / false / 0 が黙って空マッピング扱いになる穴を塞いだ
3geminilib/devbase/project/config.pyread_text()majorencoding="utf-8" を明示 + UnicodeDecodeError を「UTF-8 で保存してください」と案内する ConfigError に変換
4geminitests/project/test_config.pywrite_project_ymlminorwrite_text(..., encoding="utf-8") を指定

補足

  • docs: READMEからprivateレジストリ2件を削除 #3 について: UnicodeDecodeErrorValueError のサブクラスで既存の except OSError では捕捉できず、ConfigError に包まれないまま素通りしていました。エンコーディング明示とあわせてこの経路も塞いでいます。
  • 回帰テストを追加しました: test_boolean_version_is_an_error (bool version)、test_non_mapping_defaults_is_an_error (非マッピング defaults)、test_null_defaults_is_treated_as_empty (defaults: null は未指定と同じ)。
  • PR 本文の Test plan の件数も実測値へ更新済みです。

再レビューをお願いします。

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | codex | REQUEST_CHANGES

スキーマ版と wire format の不正値を黙って正規化せず、境界で拒否するよう修正してください。

Comment threadlib/devbase/project/config.py Outdated
Comment threadlib/devbase/project/config.py

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 2 | gemini | APPROVE

PR の変更は的確であり、YAML のエッジケース(true の評価や falsy 値の扱い)が正しく修正されていることを確認しました。
一点、今回追加されたエラーハンドリング(UnicodeDecodeError)について、その振る舞いを保証するテストの追加をご検討ください。

Comment threadlib/devbase/project/config.py
- version は `type(...) is int` で判定。YAML の `1.0` は float だが `1.0 == 1`
を満たすため、これまで schema version 1 として受理されていた
- decode_repo_plan で init 列が `1`/`0` 以外なら ConfigError。壊れた値や将来の
未知値を「init しない」として黙って通さない
- 非 UTF-8 (cp932) の project.yml が UTF-8 保存の案内を出すことをテストで固定
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@takemi-ohama

Copy link
Copy Markdown
ContributorAuthor

🔧 /ndf:fix サマリ (round 2)

対応件数: critical=0 / major=0 / minor=3 (合計 3 件)
deferred: 0 件 / rejected: 0 件
commit: b4fa776
CI: NONE (この PR は release/PLAN32 宛てで、.github/workflows/ci.ymlmain 宛て PR のみ発火するため check なし)

uv run pytest 全 1293 件 green(tests/project/ は 42 → 49 件)。

詳細

#指摘元箇所再判定対応
1codexconfig.py version 検証minortype(version) is not int へ変更。version: 1.0 は float だが 1.0 == 1 で受理されていた。bool も型判定で同時に弾けるため判定式が 1 本に減った
2codexconfig.pydecode_repo_plan()minorinit 列が 1/0 以外なら ConfigError。破損値・未知値が「init しない」に丸められ ./init.sh が黙ってスキップされる穴を塞いだ
3geminiconfig.pyUnicodeDecodeError 経路minorround 1 で追加した UTF-8 案内の失敗系テストを追加(cp932 で保存したファイルを与える)

補足

  • OSS整備: CONTRIBUTING / CHANGELOG / テンプレート / CI / レジストリ案内 #1: type(...) is int は bool を部分型として受理しないため、round 1 で入れた isinstance(version, bool) の明示チェックは不要になりました。テストも test_boolean_version_is_an_errortest_non_integer_version_is_an_error に拡張し、True / False / 1.0 / "1" をパラメータ化しています。
  • chore: claudbエイリアス整理と.serena/.gitignore追加 #2: 列数検証 (len(fields) != 4) と同じ境界で弾く形に揃えました。"" / "2" / "true" / "0 " の拒否をテストで固定しています。
  • いずれも「無効値を黙って正規化せず境界で拒否する」という round 2 codex レビューの主旨に沿った修正です。PR 本文の Test plan の件数・網羅ケースも実測値へ更新しました。

deferred / rejected はありません。再レビューをお願いします。

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 3 | codex | REQUEST_CHANGES

wire format を Bash の想定 consumer で欠損なく読み取れるよう修正してください。

Comment threadlib/devbase/project/config.py
Comment threadlib/devbase/project/config.py Outdated

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 3 | gemini | APPROVE

設計意図(ホスト側での YAML 解析による依存排除や設定の集約)と、パス・機密情報等のセキュリティ要件が適切に実装されています。テストも境界値・失敗系が網羅されており高い品質です。

YAML の値の型に関するエラーメッセージの正確性について 1 点だけマイナーな提案を残しました。

Comment threadlib/devbase/project/config.py Outdated
タブ区切りは bash の既定 IFS と同じ空白類のため、`IFS=$'\t' read -r url dir
branch init` では連続区切りが 1 つに畳まれ、branch 未指定 (空フィールド) の行で
init の値が branch にずれ込んでいた。区切りを US (\x1f) に変えると空白類ではない
ため空フィールドが保持され、bash 側の素直な読み方がそのまま正しく動く。
あわせて符号化結果に末尾 LF を付ける。`while read` は EOF 直前の改行なし行を
読み捨てるため、末尾 LF が無いと repo 1 件構成で唯一の行が丸ごと落ちていた。
契約 (区切りは \x1f / 行区切りは LF で末尾にもあり / 各フィールドは検証済みで
空白・制御文字を含まない) を docstring と PLAN32 の wire format 節に明記し、
実際の bash を `bash -c` で起動して読ませる回帰テストを追加した (Docker 不要)。
また YAML が int として読む `repo: 123` に「必須です」と出て紛らわしかったため、
未指定と型不一致でエラーメッセージを分けた。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@takemi-ohama

Copy link
Copy Markdown
ContributorAuthor

🔧 /ndf:fix サマリ (round 3)

対応件数: critical=0 / major=2 / minor=1 (合計 3 件)
deferred: 0 件 / rejected: 0 件
commit: bc94688
CI: NONE (この PR は release/PLAN32 宛てで、.github/workflows/ci.ymlmain 宛て PR のみ発火するため check なし)

uv run pytest 全 1299 件 green(tests/project/ は 49 → 55 件)。

詳細

#指摘元箇所再判定対応
1codexconfig.py wire format の区切りmajor区切りをタブ → US (\x1f)。タブは bash の既定 IFS と同じ空白類のため空フィールドが畳まれ、branch 未指定行で init が branch にずれ込んでいた
2codexconfig.pyencode_repo_plan() の末尾改行major符号化結果に末尾 LF を付与。while read は EOF 直前の改行なし行を読み捨てるため、1 repo 構成では唯一の行が丸ごと落ちていた
3geminiconfig.py_require_token()minor未指定と型不一致でエラーメッセージを分離。repo: 123 は YAML が int として読むため「repo は必須です」と出て紛らわしかった

補足

いずれも codex の指摘どおりで、手元の bash 3.2 で再現を取ってから直しました。

# 1. タブ区切り + IFS=$'\t' → 列がずれる
url=[https://x/a.git] dir=[carmo] branch=[1] init=[]
# 2. 末尾 LF なし → ループ本体に一度も入らない
(loop done)
  • OSS整備: CONTRIBUTING / CHANGELOG / テンプレート / CI / レジストリ案内 #1 の方針: 空値を非空 sentinel で表す案は entrypoint 側に「sentinel を空へ戻す」変換が増えるため採らず、区切りを空白類でない \x1f に変えて bash の素直な読み方 (IFS=$'\x1f' read -r url dir branch init) がそのまま正しく動く形にしました。各フィールドは _require_token() が空白・制御文字を弾き済みなので、値と区切りが衝突することはありません。
  • chore: claudbエイリアス整理と.serena/.gitignore追加 #2 の方針: consumer 側で EOF 行を拾う書き方 (|| [ -n "$line" ]) もありますが、素朴な while read でも安全な方を契約にしたいので producer 側で末尾 LF を付けました。decode_repo_plan() は空行を無視するため round trip には影響しません。
  • 契約テスト: 実際の bash を bash -c で起動して符号化結果を読ませる回帰テストを 2 件追加しました(Docker 不要、bash が無い環境では skip)。
    • test_bash_consumer_reads_every_column_including_empty_branch — branch 未指定行でも init がずれない
    • test_bash_consumer_does_not_drop_the_last_line_for_a_single_repo — 1 repo 構成で最後の行が落ちない
    • 区切りをタブへ戻すと両方落ちることも確認済み
  • 契約の明文化: 「フィールド区切りは \x1f」「行区切りは LF、末尾にも LF」「各フィールドは検証済みで空白・制御文字を含まない(エスケープ不要)」を encode_repo_plan() の docstring と issues/PLAN32_multi-repo-project.md の wire format 節の両方に書きました。Task 3 (entrypoint, PR feat: PLAN32-entrypoint entrypoint の複数リポジトリ clone と workspace 生成 #106) はこの契約に従います。
  • PR 本文の設計判断・Test plan も新しい wire format へ更新済みです。

deferred / rejected はありません。再レビューをお願いします。

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 4 | codex | APPROVE

修正が必要な事項はありません。

@takemi-ohamatakemi-ohama left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 cross-review | round 4 | gemini | APPROVE

全体として、yaml パーサーへの依存をホスト側に留め、bash との契約に \x1f 区切りを採用した設計は堅牢です。テストも十分でよく構成されています。数点の細かいエッジケースについてのみインラインでコメントしました。

Comment threadlib/devbase/project/config.py
Comment threadlib/devbase/project/config.py
- 省略可能な branch に `branch: ""` を書くと「必須です」と返り矛盾していたため、
未指定 (None) / 型不一致 / 空文字 の 3 つをそれぞれのメッセージで書き分ける
- `isspace()` だけでは NUL・BEL・DEL やゼロ幅空白がすり抜け、encode_repo_plan の
「制御文字を一切含まない」という契約を満たせていなかったので、印字できない文字も
まとめて弾くようにした
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@takemi-ohama

Copy link
Copy Markdown
ContributorAuthor

🔧 最終スイープ (/ndf:fix 104)

cross-review round 4 は両 AI APPROVE で収束しましたが、その最終ラウンドで gemini が付けた minor インラインコメント 2 件はループ内 fix を通っていないため、本スイープで拾いました。

対応件数: critical=0 / major=0 / minor=2 (合計 2 件、いずれも修正)
deferred: 0 件 / rejected: 0 件
commit: cf2d07a
open review thread: 0 件
テスト: uv run pytest1305 passed

詳細

#箇所指摘対応
1lib/devbase/project/config.py_require_token省略可能な branchbranch: "" を書くと「必須です」と返り矛盾する修正。「未指定 (None)」「型不一致」「空文字」を書き分け、空文字は ... に空文字は指定できません
2同上docstring は「制御文字を一切含まない」と言うが isspace() では \x00\x08 / \x7f がすり抜ける修正。c.isspace() or not c.isprintable() に変更し、ゼロ幅空白も併せて排除

判断メモ

  • OSS整備: CONTRIBUTING / CHANGELOG / テンプレート / CI / レジストリ案内 #1 は「空文字を None に丸める」案ではなく 明示エラー を選択。本 PR は後方互換を意図的に持たない方針で、丸めると branch: ""branch 未記載が同義になり、変数展開の失敗などで空になった記述ミスを検出できなくなるため。
  • chore: claudbエイリアス整理と.serena/.gitignore追加 #2 の指摘は正しいが、wire format の区切りである US (\x1f) は Python の str.isspace()True を返すため変更前から弾けており、wire format の実害は無かった。ただし encode_repo_plan の契約 (docstring) と実検証の乖離は残っていたので解消した。isprintable() 単独では半角スペースが通るため両者の OR にしている。

追加テスト

  • test_empty_repo_reports_an_empty_value_not_a_missing_field
  • test_empty_optional_branch_is_rejected_as_empty_not_as_missing
  • test_fields_reject_non_whitespace_control_characters (\x00 / \x07 / \x7f / ゼロ幅空白)
  • test_fields_reject_whitespace_and_separators に US (\x1f) ケースを追加

# Conflicts:
#	issues/PLAN32_multi-repo-project.md
@takemi-ohama
takemi-ohama marked this pull request as ready for review August 22, 2026 16:03
@takemi-ohama
takemi-ohama merged commit 27d2a5c into release/PLAN32Aug 22, 2026
@takemi-ohama
takemi-ohama deleted the feature/PLAN32-config-loader branch August 22, 2026 16:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@takemi-ohama