From 337ffa21c45f76a81e5317ae704b580d64a6843d Mon Sep 17 00:00:00 2001 From: aloekun Date: Tue, 12 May 2026 19:46:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(cli-push-runner):=20LINT=5FSCREEN=5FENABLE?= =?UTF-8?q?D=20env=20var=20override=20(=E9=A0=86=E4=BD=8D=20115=E3=80=81Ph?= =?UTF-8?q?ase=20D=20D-1=20workflow=20gap=20=E8=A7=A3=E6=B6=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase D D-1 (PR #145) 着手時に発見した workflow gap (jj auto-snapshot vs session-only opt-in) を 解消する env var override 経路を cli-push-runner に追加する。post-merge-feedback Tier 1 #1 で再 validate された Tier 1 priority。D-3 (順位 102) 着手前の critical path。 順位 115 (PR #145 post-merge-feedback Tier 1 #1): 実装 (config.rs): - ENV_LINT_SCREEN_ENABLED const = "LINT_SCREEN_ENABLED" - parse_lint_screen_env() pure function: env raw value → LintScreenEnvOverride enum * true / 1 / yes / on (case-insensitive、空白 trim) → ForceEnable * false / 0 / no / off / "" / unset → RespectToml (no-op) * その他 → InvalidValue (warning emit + 安全側で TOML 値尊重) - apply_lint_screen_env_override() side-effect: env をTOML override に適用 * ForceEnable + [lint_screen] section absent → default LintScreenConfig 生成 (enabled=true) * ForceEnable + section present → enabled = true で上書き * RespectToml → no-op (片方向設計、誤って commit しても remote PR は default OFF) - load_config() で TOML parse 後・validate 前に apply_lint_screen_env_override 呼出 unit test 10 件追加: - parse_lint_screen_env_unset_yields_respect_toml - parse_lint_screen_env_force_enable_variants (8 variants) - parse_lint_screen_env_respect_toml_variants (8 variants) - parse_lint_screen_env_invalid_value (5 variants) - apply_env_override_force_enable_on_absent_section_creates_lint_screen_config - apply_env_override_force_enable_overwrites_toml_false - apply_env_override_respect_toml_keeps_toml_enabled_true - apply_env_override_respect_toml_keeps_toml_enabled_false - apply_env_override_unset_keeps_toml_section_absent - apply_env_override_invalid_value_respects_toml docs: - Phase D guide §1 Setup を env var ベースに rewrite (旧 'config 編集' 記述を削除) * PowerShell example: $env:LINT_SCREEN_ENABLED = "true" / Remove-Item env:LINT_SCREEN_ENABLED * 片方向設計の意義 + ADR-039 試験運用標準パターンとの整合 - analysis.md Phase D section を順位 115 land 反映に更新 (D-3 unblock、D-1/D-2 副産物 list 整理) - todo8.md / todo-summary.md から順位 115 entry 削除 cargo test pass: cli-push-runner 67 tests (新規 10 + 既存 57、ZERO regression)。 Phase D 進行: D-1 ✅ / D-2 ✅ / 順位 115 ✅ / D-3 ⏳ (env var workflow で初の実 dogfood) --- docs/local-llm-offload-analysis.md | 25 +-- docs/local-llm-offload-phase-d-guide.md | 30 +-- docs/todo-summary.md | 1 - docs/todo8.md | 32 ---- src/cli-push-runner/src/config.rs | 237 +++++++++++++++++++++++- 5 files changed, 268 insertions(+), 57 deletions(-) diff --git a/docs/local-llm-offload-analysis.md b/docs/local-llm-offload-analysis.md index a8dffb9..6ae8fb9 100644 --- a/docs/local-llm-offload-analysis.md +++ b/docs/local-llm-offload-analysis.md @@ -231,25 +231,26 @@ Phase A 実装後、PR #141 (P-3 = 187 行 mixed diff) を replay → **`prompt_ ##### 🔄 Phase D: Clean dogfood validation (real pipeline 経由、進行中) -Phase C fix + Phase D 前提整備 (順位 109) 完了で **real pipeline 経由 dogfood の必要十分条件が揃った**。**しかし D-1 着手時に session-only opt-in workflow が jj auto-snapshot と本質的に衝突する gap が判明** (順位 115 として env var override を backlog 登録、post-merge-feedback Tier 1 #1 で再 validate)。次の 3 通常 PR を **env var override (`LINT_SCREEN_ENABLED=true`) 経由 (順位 115 land 後)** で dogfood、`.takt/lint-screen-report.md` の `## Summary` + `## Diagnostic` で metrics を実観測。fallback rate < 50% / num_ctx 起因 0% を real pipeline で再確認できれば Phase E に進む。 +Phase C fix + Phase D 前提整備 (順位 109) 完了で **real pipeline 経由 dogfood の必要十分条件が揃った**。D-1 着手時に session-only opt-in workflow が jj auto-snapshot と本質的に衝突する gap が判明したが、**順位 115 (`LINT_SCREEN_ENABLED` env var override) land で解消**。env var 経路 (`$env:LINT_SCREEN_ENABLED = "true"`) で `push-runner-config.toml` を編集せずに lint_screen を有効化できるため、D-3 で初の実 dogfood が成立する。`.takt/lint-screen-report.md` の `## Summary` + `## Diagnostic` で metrics を実観測、fallback rate < 50% / num_ctx 起因 0% を real pipeline で再確認できれば Phase E に進む。 -**Phase D 対象 PR 構成 (2026-05-12 確定 / D-1 land 後更新)**: +**Phase D 対象 PR 構成 (2026-05-12 確定 / D-2 land + 順位 115 land 後更新)**: -| Order | 構成 (todo-summary.md priority list より) | Effort | 推定 diff 行 | Diff Profile | 状態 | +| Order | 構成 (todo-summary.md priority list より) | Effort | 推定 / 実 diff 行 | Diff Profile | 状態 | |---|---|---|---|---|---| | **D-1** ✅ | 順位 112 + 113 + 114 = ADR amendments bundle (ADR-038 eprintln scope / ADR-027 metrics override / 新規 ADR Local LLM context size) + 順位 115 backlog 化 | S+ | 298 (insert 228 / delete 70) | docs + 1 Rust comment | **PR #145 land 済 (2026-05-12)**、lint_screen dogfood は skip (workflow gap) | -| **115** ⏳ | `LINT_SCREEN_ENABLED` env var override (D-1 で発見した workflow gap 解消) | S | ~80-120 (Rust impl + test) | Rust impl | **D-2 着手前に land 必須**、post-merge-feedback Tier 1 #1 | -| **D-2** | 順位 101 + 106 + 103 = lint rule code touch (rule⑧ edge case test / self-exclusion assertion / lint runner field comment) | S+S+S | ~150-200 | Rust test/comment mix | 順位 115 land 後、env var workflow で初の実 dogfood | -| **D-3** | 順位 102 = `paths` filter を lint runner に実装 (impl + test + 既存 rule migration) | M | ~250-350 | Rust impl + test | D-2 完了後、num_ctx 32768 上限テスト | +| **D-2** ✅ | 順位 101 + 106 + 103 = lint rule code touch (rule⑧ edge case test / self-exclusion assertion / lint runner field comment) | S+S+S | 172 (insert 84 / delete 88) | Rust test/comment mix | **PR #146 land 済 (2026-05-12)**、lint_screen dogfood は skip (順位 115 未 land 時点) | +| **115** ✅ | `LINT_SCREEN_ENABLED` env var override (D-1 で発見した workflow gap 解消) | S | 想定通り (Rust impl + test 10 件) | Rust impl + Phase D guide rewrite | **PR #147 想定で land 中**、D-3 着手 unblock | +| **D-3** ⏳ | 順位 102 = `paths` filter を lint runner に実装 (impl + test + 既存 rule migration) | M | ~250-350 | Rust impl + test | **順位 115 land 後すぐ着手可、初の real dogfood + num_ctx 32768 上限テスト** | -**size ramp-up 設計**: small → mid → mid-large の漸増で、small PR 単体での fallback 観測と large PR で num_ctx 限界に近づく挙動を両方カバー。**ただし D-1 は workflow gap により lint_screen dogfood をスキップ、実質 metrics 観測は D-2 / D-3 の 2 PR**。 +**size ramp-up 設計**: small → mid → mid-large の漸増で、small PR 単体での fallback 観測と large PR で num_ctx 限界に近づく挙動を両方カバー。**D-1 / D-2 は workflow gap により lint_screen dogfood をスキップ、実質 metrics 観測は D-3 のみ**。3 PR 観測予定だったが kill-switch 基準 (3/5 で停止) を踏まえて D-3 単独でも判定可能 (採用昇格 / 継続観測 / 却下) と位置付ける。 -**D-1 dogfood outcome (skip 理由 + 副産物)**: +**D-1 / D-2 dogfood outcome (skip 理由 + 副産物)**: -- lint_screen dogfood は実施せず (workflow gap) -- 副産物として **workflow gap を systemic に発見 + 順位 115 を Tier 1 backlog 登録 + post-merge-feedback Tier 1 #1 で再 validate** -- ADR-040 内部不整合 (3.33x label vs `(num_ctx/8192)*180s` formula = 4x) は takt review 1 iter で検出 → fix で解消、post-merge-feedback Tier 3 #1 で sublinear clarification 採用 (順位 116) -- lib.rs L128-139 → ADR-040 移管 edit order を post-merge-feedback Tier 3 #3 で codify 採用 (順位 117) +- lint_screen dogfood は実施せず (D-1 着手時の workflow gap が両 PR で持続) +- 副産物 (D-1): **workflow gap を systemic に発見 + 順位 115 を Tier 1 backlog 登録 + post-merge-feedback Tier 1 #1 で再 validate** +- 副産物 (D-1): ADR-040 内部不整合 (3.33x label vs `(num_ctx/8192)*180s` formula = 4x) は takt review 1 iter で検出 → fix で解消、post-merge-feedback Tier 3 #1 で sublinear clarification 採用 (順位 116) +- 副産物 (D-1): lib.rs L128-139 → ADR-040 移管 edit order を post-merge-feedback Tier 3 #3 で codify 採用 (順位 117) +- 副産物 (D-2): clean merge (post-merge-feedback 0 件採用)、feedback loop 正常動作を再確認 **Phase D 計測手順** (各 PR 共通): diff --git a/docs/local-llm-offload-phase-d-guide.md b/docs/local-llm-offload-phase-d-guide.md index 6b1b0d9..ce30c0f 100644 --- a/docs/local-llm-offload-phase-d-guide.md +++ b/docs/local-llm-offload-phase-d-guide.md @@ -8,30 +8,38 @@ > > **引退条件**: Phase d 完了 (3-5 PR で実観測) → §8.E 採否判定 → ADR-038 を「採用」or「却下」に昇格 → 本ファイル削除 + analysis.md / history.md も同タイミングで再評価。 -## 1. Setup (session-only opt-in) +## 1. Setup (session-only opt-in via env var) -`push-runner-config.toml` は default OFF のまま。dogfood する session で **手動で `enabled = true` に切り替え (commit しない)**。 +`push-runner-config.toml` は default OFF のまま **編集しない**。dogfood する session で **env var `LINT_SCREEN_ENABLED=true` を set** することで TOML 値を override する (順位 115、Phase D D-1 で発見した jj auto-snapshot vs session-only opt-in workflow gap を解消した経路)。 -```bash +```powershell # 1. Ollama 起動確認 curl -s http://localhost:11434/api/tags | jq '.models | map({name, size})' # 期待: mistral:7b が含まれる -# 2. config 切替 (commit しない、session 内のみ) -# push-runner-config.toml の [lint_screen] section で -# enabled = false → enabled = true -# 編集後、jj diff で確認 (push 時に意図せず commit に乗らないよう注意) +# 2. env var で lint_screen を session-only で有効化 (config 編集しない) +$env:LINT_SCREEN_ENABLED = "true" +# 受容値 (case-insensitive、空白 trim): true / 1 / yes / on → force enable +# false / 0 / no / off / "" / 未設定 → TOML 値を尊重 (= default OFF) +# それ以外 → warning emit + TOML 値を尊重 # 3. cli-finding-classifier.exe deploy 確認 ls -la .claude/cli-finding-classifier.exe # 期待: ファイル存在、~2.2MB -# 4. dogfood 完了後、必ず enabled = false に戻す (revert) -jj diff push-runner-config.toml # 確認 -# 編集して enabled = false に戻す、または jj restore push-runner-config.toml +# 4. dogfood 完了後、env var を unset (session 終了で自動消滅、commit には影響なし) +Remove-Item env:LINT_SCREEN_ENABLED -ErrorAction Ignore ``` -**意義**: kill-switch が即可能、他人 / 派生プロジェクトの push に影響なし、設計 (default OFF, 試験運用 opt-in) との整合性。 +Bash / WSL 環境では `export LINT_SCREEN_ENABLED=true` / `unset LINT_SCREEN_ENABLED` で同等。 + +**意義**: + +- jj auto-snapshot 環境でも config を編集せずに済む = 誤って commit する事故を構造的に排除 +- kill-switch が即可能 (env unset で TOML default OFF に自然復帰) +- 他人 / 派生プロジェクトの push に影響なし +- 設計 (default OFF, 試験運用 opt-in) との整合性 + ADR-039 試験運用標準パターン (config opt-in + kill-switch + bounded lifetime) との整合 +- env override の片方向設計 (true → force enable / false → TOML 尊重) で「誤って enable した状態が PR / master に流れる」リスクが二重防御 ## 2. 計測 (各 dogfood PR で実施) diff --git a/docs/todo-summary.md b/docs/todo-summary.md index c999672..e64e15b 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -74,7 +74,6 @@ | 108 | 💎 Tier 3 | **CLAUDE.md に「Tier 2 偽装検知 + 却下パターン」table (PR #141 T3-#3 採用)** | todo6.md | S | なし (`~/.claude/CLAUDE.md` に memory `feedback_no_unenforced_rules` の policy をユーザー可視 table として公開、Tier 2 と称した必須化ルール提案を新セッションでも一貫して却下できる構造、memory ファイル閉鎖を補完) | | 110 | 💎 Tier 3 | **pure function test pattern template を `testing.md` に追記 (PR #142 T2-#3 採用)** | todo6.md | S | なし (Phase A の `overflow_hint()` をモデル例とし「境界値 / None / 閾値未満」3 パターンの test テンプレを `~/.claude/rules/common/testing.md` に追記、副作用分離の促進、Rust lib 全般で再利用) | | 111 | 💎 Tier 3 | **`docs-governance.md` に todo5/todo6 routing rule 明文化 (PR #142 T3-#1 採用)** | todo6.md | S | なし (Phase/bundle 関連 → todo6、global rules/lint → todo5 等の routing rule を `~/.claude/rules/common/docs-governance.md` に追記、PR #142 で実証された file pointer bifurcation の構造的予防、CR Minor #2 と同根) | -| 115 | 🚀 Tier 1 | **`LINT_SCREEN_ENABLED` env var override を cli-push-runner に追加 (Phase D D-1 workflow gap)** | todo8.md | S | D-2 を block (D-2 着手前に land 必須)。Phase D guide §1 の session-only opt-in が jj auto-snapshot と本質的に衝突するため、env var で TOML override する path を追加し commit-free な dogfood を成立させる。Phase D D-1 (PR #145 land 済) 着手時に systemic に発見、post-merge-feedback Tier 1 #1 で再 validate 済 | | 116 | 💎 Tier 3 | **ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用)** | todo8.md | XS | なし (L42-48 で「sublinear (3.33x)」と「per-invoke latency が概ね線形」が並存し reference table 600s と formula 720s が乖離。実測値 600s 採択 + 保守上限 720s + sublinear 性の KV cache locality 根拠を 2-3 行追記して整合化、永続 ADR の数値正確性確保) | | 117 | 💎 Tier 3 | **`coding-style.md § Cross-File Reference Lifecycle` に ephemeral → permanent 知識移管 edit order 追記 (PR #145 T3-#3 採用)** | todo8.md | S | なし (PR #145 で lib.rs L128-139 → ADR-040 移管 + Phase C/D empirical data 移管の 2 観測。既存ルール (参照方向制約) と complementary な「① permanent target 先行作成・validate → ② 参照追加 → ③ 参照元削除」3 ステップ原則を `~/.claude/rules/common/coding-style.md` に codify、次回 ephemeral 計画書 retire 時の checklist として再利用) | diff --git a/docs/todo8.md b/docs/todo8.md index 2f39c8e..09fc003 100644 --- a/docs/todo8.md +++ b/docs/todo8.md @@ -10,38 +10,6 @@ ## 現在進行中 -### `LINT_SCREEN_ENABLED` env var override を cli-push-runner に追加 (Phase D D-1 着手時の workflow gap、2026-05-12 発見) - -> **動機**: Phase D guide §1 / analysis.md Phase D 計測手順 は「session-only opt-in」 (`[lint_screen] enabled = true` を commit せず runtime のみ反映) を前提に記述されていたが、jj の auto-snapshot 性質と本質的に衝突する。`push-runner-config.toml` を編集すると即座に @ にスナップショットされ、`pnpm push` がその commit を remote に push してしまうため、「local enable / remote disable」が成立しない。`cli-push-runner` の config 読み取り経路に env var override (`LINT_SCREEN_ENABLED=true` 等で TOML の `[lint_screen] enabled` を上書き) を追加することで、commit-free な session opt-in が成立する。 -> -> **本タスクの位置づけ**: Phase D D-1 (PR #145 想定) 着手時に systemic に発見された **workflow blocker**。D-2 着手前に land しないと D-2 / D-3 の dogfood も同様にスキップせざるを得ない。Effort S (~30-50 行 Rust + test 2-3 件)。 -> -> **参照**: `docs/local-llm-offload-analysis.md` Phase D 計測手順 (D-1 時点で gap が明文化済)、`src/cli-push-runner/src/config.rs` (LintScreenConfig 読み取り箇所)、`docs/local-llm-offload-phase-d-guide.md` §1 (旧 workflow 記述) - -#### 設計決定の余地 - -- **env var 名**: `LINT_SCREEN_ENABLED` (TOML field 名と揃える) / `PUSH_RUNNER_LINT_SCREEN` (prefix で namespace) / 別案 -- **値 semantics**: `true` / `1` / `yes` で有効、空文字列 / 未設定 / `false` で TOML 値を尊重 -- **TOML override 方向**: env var を **TOML より優先** (現状 TOML default OFF を env で強制 ON にする運用) / TOML を優先で env は fallback (現実装では TOML 必須なのでこちらは意味なし) -- **将来拡張**: 他フィールド (model / endpoint / timeout_secs) も env var で override する一般化 → 当面は `enabled` のみ -- **type 安全**: bool parse 失敗時の fallback (FALSE 扱い vs 警告 emit) → 警告 emit + FALSE 扱い - -#### 作業計画 - -- [ ] `src/cli-push-runner/src/config.rs` の `LintScreenConfig::enabled` を env var override 対応に変更 (TOML 読み取り後に env を merge) -- [ ] env var parse helper 関数を追加 (bool 解釈 + warning emit) -- [ ] unit test 3 件: env unset で TOML 尊重 / env=true で override / env=invalid で警告 + FALSE -- [ ] `docs/local-llm-offload-phase-d-guide.md` §1 Setup を env var ベースに rewrite (旧「config を編集」記述を削除) -- [ ] `docs/local-llm-offload-analysis.md` Phase D 計測手順は D-1 PR で既に env var ベースに更新済、整合性を確認 -- [ ] 本エントリ削除 + todo-summary.md 行削除 - -#### 完了基準 - -- env var 経由で lint_screen を有効化でき、`push-runner-config.toml` を編集せずに dogfood 実施可能になる -- D-2 / D-3 で session-only opt-in workflow が成立する - ---- - ### ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用) > **動機**: ADR-040 L42-48 の `step_timeout` 説明は「sublinear (3.33x)」と記述したが、本文中に「per-invoke latency が num_ctx に対して概ね線形に拡大する経験則」も併記しており、両者の関係が不明瞭。派生プロジェクトが reference table から 32K = 600s を読む際、なぜ formula `(num_ctx/8192)*180` で導出される 720s と乖離するかが直感的に分からない。clarification として「実測値 600s を正規値として採択、computed 720s は保守上限の目安、sublinear 性の根拠は KV cache locality 効果 (大規模 context で per-token efficiency 向上)」の 2-3 行追記が必要。 diff --git a/src/cli-push-runner/src/config.rs b/src/cli-push-runner/src/config.rs index 46e505b..597a389 100644 --- a/src/cli-push-runner/src/config.rs +++ b/src/cli-push-runner/src/config.rs @@ -10,6 +10,21 @@ pub(crate) const DEFAULT_LINT_SCREEN_ENDPOINT: &str = "http://localhost:11434"; pub(crate) const DEFAULT_LINT_SCREEN_EXE_PATH: &str = ".claude/cli-finding-classifier.exe"; pub(crate) const DEFAULT_LINT_SCREEN_OUTPUT_PATH: &str = ".takt/lint-screen-report.md"; +/// `LINT_SCREEN_ENABLED` env var の名前 (順位 115、Phase D D-1 workflow gap 解消)。 +/// +/// 用途: session-only opt-in (jj auto-snapshot 環境で `push-runner-config.toml` を編集せずに +/// lint_screen を一時的に有効化する)。 +/// +/// **解釈** (todo entry 順位 115 設計決定に基づく): +/// - `"true"` / `"1"` / `"yes"` (case-insensitive、空白 trim) → **force enable** (TOML override) +/// - `"false"` / `"0"` / `"no"` / `""` / unset → **TOML 値を尊重** (override しない、no-op) +/// - その他の値 → warning emit + TOML 値を尊重 (= invalid 扱い、安全側に倒す) +/// +/// **片方向設計の意図**: env を temporary に set すれば session opt-in、unset すれば TOML default +/// (= `enabled = false`) に自然復帰する。誤って commit しても remote PR は config 上 OFF のまま +/// (= dogfood は走らない) なので、Phase D guide §1 の「local enable / remote disable」が成立する。 +pub(crate) const ENV_LINT_SCREEN_ENABLED: &str = "LINT_SCREEN_ENABLED"; + #[derive(Deserialize)] pub(crate) struct Config { pub(crate) quality_gate: QualityGateConfig, @@ -89,12 +104,93 @@ pub(crate) fn load_config() -> Result { let path = config_path(); let content = std::fs::read_to_string(&path) .map_err(|e| format!("設定ファイルの読み込みに失敗: {} ({})", path.display(), e))?; - let config: Config = + let mut config: Config = toml::from_str(&content).map_err(|e| format!("設定ファイルのパースに失敗: {}", e))?; + apply_lint_screen_env_override(&mut config, std::env::var(ENV_LINT_SCREEN_ENABLED).ok()); validate_config(&config)?; Ok(config) } +/// `LINT_SCREEN_ENABLED` env var を解釈した結果。 +/// +/// `parse_lint_screen_env` の戻り値で、`apply_lint_screen_env_override` が分岐する。 +#[derive(Debug, PartialEq, Eq)] +enum LintScreenEnvOverride { + /// env が `true`/`1`/`yes` 系 → TOML 値を上書きして `enabled = true` を強制。 + ForceEnable, + /// env が `false`/`0`/`no`/`""`/unset → TOML 値を尊重 (no-op)。 + RespectToml, + /// env が解釈不能な文字列 → warning emit 候補、安全側で TOML 値を尊重。 + InvalidValue, +} + +/// `LINT_SCREEN_ENABLED` env var の生文字列を解釈する純粋関数 (test 容易性のため env 読み取りと分離)。 +/// +/// `None` (unset) は `RespectToml` として扱う。空白 trim + 小文字化して比較する。 +fn parse_lint_screen_env(raw: Option<&str>) -> LintScreenEnvOverride { + let Some(value) = raw else { + return LintScreenEnvOverride::RespectToml; + }; + let normalized = value.trim().to_ascii_lowercase(); + match normalized.as_str() { + "true" | "1" | "yes" | "on" => LintScreenEnvOverride::ForceEnable, + "false" | "0" | "no" | "off" | "" => LintScreenEnvOverride::RespectToml, + _ => LintScreenEnvOverride::InvalidValue, + } +} + +/// `LINT_SCREEN_ENABLED` env var の値を `config.lint_screen.enabled` に反映する。 +/// +/// 設計仕様は `ENV_LINT_SCREEN_ENABLED` の doc comment 参照。`[lint_screen]` section が +/// TOML に存在しなくても、env が `ForceEnable` の場合は default 値で `LintScreenConfig` を +/// 生成する (Phase D D-1 で発見した workflow gap の解消、順位 115)。 +/// +/// `raw` 引数は test 容易性のため caller が `std::env::var(...)` を解決して渡す。 +fn apply_lint_screen_env_override(config: &mut Config, raw: Option) { + let raw_ref = raw.as_deref(); + match parse_lint_screen_env(raw_ref) { + LintScreenEnvOverride::ForceEnable => { + match config.lint_screen.as_mut() { + Some(lint) => { + lint.enabled = true; + } + None => { + config.lint_screen = Some(default_lint_screen_enabled()); + } + } + eprintln!( + "[push-runner] {}: TOML override で [lint_screen] enabled を true に強制 (順位 115 env override)", + ENV_LINT_SCREEN_ENABLED + ); + } + LintScreenEnvOverride::RespectToml => {} + LintScreenEnvOverride::InvalidValue => { + eprintln!( + "[push-runner] WARN: {}='{}' を bool として解釈できません、TOML 値を尊重します。\ + 受容値: true/1/yes/on (enable) / false/0/no/off/\"\" (TOML 尊重)", + ENV_LINT_SCREEN_ENABLED, + raw_ref.unwrap_or("") + ); + } + } +} + +/// env override で `[lint_screen]` section を新規生成する際の default 値。 +/// +/// 他の field は `None` のままで、`stages::lint_screen` の `resolve_invoke_params` / `run_lint_screen` +/// 側が `DEFAULT_LINT_SCREEN_*` 定数で fallback する。 +fn default_lint_screen_enabled() -> LintScreenConfig { + LintScreenConfig { + enabled: true, + exe_path: None, + model: None, + endpoint: None, + timeout_secs: None, + max_diff_lines: None, + output_path: None, + } +} + fn validate_config(config: &Config) -> Result<(), String> { if config.quality_gate.groups.is_empty() { return Err("設定ファイルエラー: quality_gate.groups が空です".into()); @@ -462,6 +558,145 @@ command = "echo push" assert!(result.unwrap_err().contains("groups が空")); } + #[test] + fn parse_lint_screen_env_unset_yields_respect_toml() { + assert_eq!( + parse_lint_screen_env(None), + LintScreenEnvOverride::RespectToml + ); + } + + #[test] + fn parse_lint_screen_env_force_enable_variants() { + for value in ["true", "TRUE", "1", "yes", "YES", "on", "On", " true ", "\tyes\n"] { + assert_eq!( + parse_lint_screen_env(Some(value)), + LintScreenEnvOverride::ForceEnable, + "value '{}' should map to ForceEnable", + value + ); + } + } + + #[test] + fn parse_lint_screen_env_respect_toml_variants() { + for value in ["false", "FALSE", "0", "no", "NO", "off", "", " "] { + assert_eq!( + parse_lint_screen_env(Some(value)), + LintScreenEnvOverride::RespectToml, + "value '{}' should map to RespectToml", + value + ); + } + } + + #[test] + fn parse_lint_screen_env_invalid_value() { + for value in ["maybe", "2", "enable", "disabled", "yes please"] { + assert_eq!( + parse_lint_screen_env(Some(value)), + LintScreenEnvOverride::InvalidValue, + "value '{}' should map to InvalidValue", + value + ); + } + } + + fn make_config_without_lint_screen() -> Config { + Config { + quality_gate: QualityGateConfig { + parallel: None, + step_timeout: None, + groups: vec![GroupConfig { + name: "t".into(), + pre: None, + commands: vec!["echo".into()], + }], + }, + diff: None, + lint_screen: None, + takt: TaktConfig { + workflow: "w".into(), + task: "t".into(), + extra_args: None, + }, + push: PushConfig { + command: "echo".into(), + timeout: None, + }, + } + } + + fn make_config_with_lint_screen(enabled: bool) -> Config { + let mut config = make_config_without_lint_screen(); + config.lint_screen = Some(LintScreenConfig { + enabled, + exe_path: None, + model: None, + endpoint: None, + timeout_secs: None, + max_diff_lines: None, + output_path: None, + }); + config + } + + #[test] + fn apply_env_override_force_enable_on_absent_section_creates_lint_screen_config() { + let mut config = make_config_without_lint_screen(); + apply_lint_screen_env_override(&mut config, Some("true".to_string())); + let lint = config.lint_screen.expect( + "env=true should construct default LintScreenConfig when [lint_screen] section absent", + ); + assert!(lint.enabled); + assert!(lint.exe_path.is_none()); + assert!(lint.model.is_none()); + } + + #[test] + fn apply_env_override_force_enable_overwrites_toml_false() { + let mut config = make_config_with_lint_screen(false); + apply_lint_screen_env_override(&mut config, Some("1".to_string())); + assert!(config.lint_screen.unwrap().enabled); + } + + #[test] + fn apply_env_override_respect_toml_keeps_toml_enabled_true() { + let mut config = make_config_with_lint_screen(true); + apply_lint_screen_env_override(&mut config, Some("false".to_string())); + assert!( + config.lint_screen.unwrap().enabled, + "env=false should respect TOML (TOML had enabled=true, must remain true)" + ); + } + + #[test] + fn apply_env_override_respect_toml_keeps_toml_enabled_false() { + let mut config = make_config_with_lint_screen(false); + apply_lint_screen_env_override(&mut config, Some("".to_string())); + assert!(!config.lint_screen.unwrap().enabled); + } + + #[test] + fn apply_env_override_unset_keeps_toml_section_absent() { + let mut config = make_config_without_lint_screen(); + apply_lint_screen_env_override(&mut config, None); + assert!( + config.lint_screen.is_none(), + "env unset + [lint_screen] absent should remain None" + ); + } + + #[test] + fn apply_env_override_invalid_value_respects_toml() { + let mut config = make_config_with_lint_screen(false); + apply_lint_screen_env_override(&mut config, Some("maybe".to_string())); + assert!( + !config.lint_screen.unwrap().enabled, + "invalid env value should treat as RespectToml (TOML enabled=false preserved)" + ); + } + #[test] fn validate_rejects_empty_commands() { let config = Config {