diff --git a/docs/local-llm-offload-analysis.md b/docs/local-llm-offload-analysis.md index 4270284..9cc7dbc 100644 --- a/docs/local-llm-offload-analysis.md +++ b/docs/local-llm-offload-analysis.md @@ -195,61 +195,57 @@ cargo test -p cli-finding-classifier --test lint_screen_evals -- \ # 任意の小さい diff で pnpm push して .takt/lint-screen-report.md が生成されるか確認 ``` -#### 次に何をするか (優先度順) - -1. **Phase d kickoff prep** ✅ **完了 (2026-05-10)**: 運用ガイド = [docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) として独立 doc 化。決定事項: (a) **session-only opt-in** (config commit せず session 内のみ enable / kill-switch 即可)、(b) metrics = **latency p50/p95 + fallback rate + Claude session input token 削減効果 (質的傾向)**、(c) kill-switch = **fallback rate > 50% で停止**。過去 dogfood の 3 obstacles (findings ゼロ / review body 抽出漏れ / rate-limit) は classifier 専用で lint_screen は CR 非依存のため scope 外と確定。**前提条件 (a)-(d) は PR #135 + PR #136 で全充足済** -2. **Phase d 実 dogfood** (long-running、数日〜数週間): kickoff 後の通常 PR 5 件で lint_screen の token 削減 / latency / 大規模 diff JSON 完全性を実観測。`feedback_dogfood_evals_two_phase` (evals → dogfood の 2 段階) の dogfood 段階に該当。具体運用は [docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) §1-3 に従う - - **dogfood 対象 PR roster** (`docs/todo-summary.md` から選定、size ramp-up 順序): - - | Order | 構成 | Effort | Diff Profile | dogfood signal | - |---|---|---|---|---| - | P-1 | Bundle h (順位 89+90) + Bundle g-2 (順位 87+88) ✅ **完了 (PR #139、2026-05-10)** | M | global rules markdown 4 file (project diff は ADR-039 + cross-link + todo cleanup のみ) | classifier preview のみ取得 (real pipeline 未実行)。詳細は本 table 直後の **P-1 dogfood outcome** 参照 | - | P-2 | Bundle j-1 (順位 94 — `../docs/` 相対パス detect lint rule) ✅ **完了 (PR #140、2026-05-10)** | S | TOML config + 軽い Rust regex (203 行 mixed diff) | classifier preview のみ取得 (real pipeline 未実行)。詳細は本 table 直後の **P-2 dogfood outcome** 参照 | - | ~~P-3~~ | ~~Bundle g-1 (順位 85+86)~~ ⚠️ **roster から除外** — PR #125 で land 済を P-3 着手時に発見、stale todo 削除のみで実装作業なし | — | — | — | - | P-3 (繰上げ) | Bundle d (順位 68 — no-ephemeral-todo-reference self-exclusion test) ✅ **完了 (PR、2026-05-11)** | S | Rust test only (187 行) | classifier preview のみ取得 (real pipeline 未実行)。詳細は本 table 直後の **P-3 dogfood outcome** 参照 | - | P-4 (繰上げ) | Bundle c-1 (順位 63+64+67 — cli-merge-pipeline Drop guard + reaper + ADR) | L | Rust impl ×2 + ADR | 大規模 Rust (PR #132 868 行 stress 再現候補) (旧 P-5) | - - **P-1 dogfood outcome (PR #139、2026-05-10)**: - - - **classifier preview metrics** (cli-finding-classifier 直叩き、real pipeline 経由ではない): - - latency: 23s (eval baseline p95=8.4s の ~3x、337-line diff サイズ起因の推定) - - findings: 0 (空配列) - - screen_decision: `human_review` (fallback path activated) - - fallback_reason: `JSON parse error: missing field 'screen_decision'` — num_ctx=8192 でも 337-line diff で出力 truncate の可能性 - - **Phase d 学習**: 順位 98 (`num_ctx` overflow detection diagnostic warn log) の必要性を再確認 = mistral:7b 出力崩壊を runtime hint で即診断する優先度が確定 - - **post-merge-feedback (10 findings → 1 件採用)**: T3 #1 (development-workflow.md に 「同一ファイル複数編集の 1 task 統合」 + 「partial completion + 後続 PR 追補明記」 の 2 pattern 追補) を採用 → **順位 100** として登録済。様子見 3 件 / 却下 5 件 (詳細は `.claude/feedback-reports/139.md`) - - **観測 caveat**: post-merge-feedback agent が PR #139 で初観測した `baselinebaseline` (table cell 内連続単語重複) は session/prepush 間で観測が矛盾 (jj cache stale 疑い、未確定)。Frequency Low 単独で T1 #1 連続重複単語 lint / T1 #2 jj cache validation は様子見 / 却下。3 PR 観測閾値で再評価 - - **real pipeline 経由 P-1 metric**: P-2 (Bundle j-1) 移行時に再検討 = lint_screen を session-only opt-in で動かす機会を改めて作る (commit pollution 回避と integration test の trade-off は P-2 で再判断) - - **P-2 dogfood outcome (PR #140、2026-05-10)**: - - - **classifier preview metrics** (cli-finding-classifier 直叩き、real pipeline 経由ではない、P-1 と同方針 = commit pollution 回避): - - latency: 46s (P-1 = 23s の ~2x、203-line diff にも関わらず latency 増。入力依存性 + mistral 内部状態の variance (cold/warm) 両候補) - - findings: 0 (空配列) - - screen_decision: `human_review` (fallback path activated) - - fallback_reason: 同 P-1 (`JSON parse error: missing field 'screen_decision'`、line 94 column 1) - - **Fallback rate trend (累積)**: 2/2 = 100%。Phase d guide §3 の kill-switch criteria (3/5 PR で fallback = 60% で停止) は real pipeline 経由なら **既に発動相当**。classifier preview ベースの観測で参考値、P-3 移行時に kill-switch 厳密判定の必要性を再評価 - - **Phase d 学習**: 順位 98 (`num_ctx` overflow detection diagnostic warn log) の優先度を再々確認 = Rust+TOML+MD 混合 diff (203 行) でも崩壊で diff size 起因単独ではない signal、P-3 着手前の優先実装を強く推奨 - - **post-merge-feedback (8 findings → 5 件採用)**: T1 #1/#2/#3 + T3 #1/#2 を採用 → **順位 101-105** として登録済。T2 #1 (大文字バリアント test 必須化) は不採用 = 「Tier 2 偽装の必須化ルール = unenforced rule pattern」として `feedback_no_unenforced_rules.md` に検知 signal 3 項目を追記。T2 #2 (mistral fallback 率監視) は様子見 (Phase d 3 PR 観測閾値で Tier 1 昇格再検討)。詳細は `.claude/feedback-reports/140.md` - - **real pipeline 経由 P-2 metric**: P-3 移行時に再検討 (P-1 → P-2 で本 trade-off 判断は共通結論で固定化、P-3 で改めて見直しの必要性は低いが kill-switch 100% trend を踏まえ再評価) - - **P-3 (繰上げ) dogfood outcome (PR、2026-05-11)**: - - - **classifier preview metrics** (cli-finding-classifier 直叩き、real pipeline 経由ではない、P-1/P-2 と同方針): - - latency: **11s** (P-1=23s / P-2=46s から大幅短縮、187-line diff の input + warm context 推定) - - findings: 0 (空配列) - - screen_decision: `human_review` (fallback path activated) - - fallback_reason: `JSON parse error: missing field 'screen_decision'` (line 1 column 692) - - **Fallback rate trend (累積)**: **3/3 = 100%**。Phase d guide §3 の kill-switch criteria (3/5 PR で fallback = 60% で停止) は real pipeline 経由なら **既に発動超過**。classifier preview ベースで全 3 回失敗 → 順位 98 (`num_ctx` overflow detection) を **Phase d 結果集約より先に実装** することを強く推奨 (kill-switch 厳密判定の前提整備) - - **Latency variance signal**: P-1=23s / P-2=46s / P-3=11s の振れ幅は input size と弱相関 (P-2 が最短入力で最長 latency)、**mistral 内部状態 (cold/warm context)** が支配的要因の仮説を強化。real pipeline 計測時は cold start を避ける warmup 戦略の設計が必要 - - **post-merge-feedback**: 本 PR merge 後に取得 (現時点で未実施) - - **設計判断のポイント**: - - **Effort 分布 (旧 M→S→M→S→L → 実 M→S→S→L)**: ~~前半小規模 / 後半大規模で kill-switch (fallback > 50%) signal の質を切り分け~~ 旧 P-3 (M = Bundle g-1) が PR #125 で land 済発見により roster から除外、4 PR roster に縮小。Effort 分布は M→S→S→L に変化、size ramp-up の中段で M が抜けたため小規模 (P-3) → 大規模 (P-4) の jump がやや大きい。kill-switch signal の切り分けは P-4 (L) で num_ctx 再到達検証として有効 - - **Bundle h + g-2 を 1 PR に統合**: 共通テーマ「global rules consolidation (process/lifecycle codification)」、reviewer も「rule 追加 4 件まとめ」として認識しやすい - - **Bundle f 除外**: `(defer)` 表記 = systemic 性未確認のため Phase d で push 圧力を加えない -3. **Phase d 結果集約**: 計測結果から §8.E 採用 / §8.F 着手 / kill-switch を判定。dogfood 完了後 +#### 次に何をするか (analysis.md 削除条件への critical path、2026-05-11 更新) + +> **本ファイル削除条件**: Phase d 採否判定完了 (採用 or 却下) → ADR-038 を「採用」or「却下」に昇格 → follow-up タスクを permanent artifact (ADR / global rule / todo) に移管 → retirement workflow (`~/.claude/rules/common/docs-governance.md` §Retirement Workflow を参照、global config のため URL link なし) を実行 → 本ファイル削除。 +> +> **進行方針 (2026-05-11、kill-switch 100% trend を踏まえた pivot)**: dogfood を一度止めて broken signal の repair (順位 98 = `num_ctx` overflow detection 診断) を最優先。診断 → root cause 特定 → fix → clean dogfood の順で進む。Bundle c-1 (旧 P-4) や Bundle c の他項目は **本 critical path 外** として通常 Tier 1 優先度で別途処理。 + +##### 🚀 Phase A: Diagnostic ✅ **完了 (本 PR、2026-05-11)** + +**順位 98 実装完了** = `lib-ollama-client` の `generate_json` に `OllamaMetadata` (`prompt_eval_count` / `eval_count` / `num_ctx`) を組み込み、serde parse error 時に stderr へ warn log を emit する診断層を追加。`OllamaApi` trait の `generate_with_metadata` (default fallback あり、StubOllama は変更不要)、`emit_overflow_diagnostic` 関数で 90% 以上時に「num_ctx を増やす hint」を含める。16 unit test pass、cli-finding-classifier 経由でも warn log が stderr に出ることを smoke 確認。 + +##### 🔍 Phase B: Root cause identification ✅ **完了 (Phase A 即時 dogfood、2026-05-11)** + +Phase A 実装後、PR #141 (P-3 = 187 行 mixed diff) を replay → **`prompt_eval_count: 8192 (vs num_ctx: 8192)` = 100% 到達を実機確認**。**真因 = num_ctx truncation で確定**。mistral の prompt が完全に context cap で truncate されて JSON output が完成せず `screen_decision` field 欠落の症状を引き起こしていた。仮説 2 候補 (num_ctx truncation / mistral 出力崩壊) のうち前者が真因と decisive 判定。 + +##### 🔧 Phase C: Root cause fix (1 PR、XS-S、次の next action) — C-1 経路で確定 + +- **C-1 (確定経路)**: `DEFAULT_NUM_CTX = 8192 → 16384 (or 32768)` への増加。mistral:7b の theoretical max は 32K、本リポの prompt サイズ (~4-5K) + 大規模 diff (~3-4K) で 16384 が安全マージン込みで妥当。RAM 影響評価 + lint-screen evals の regression test (15 件 fixtures が pass し続けるか) + smoke dogfood で fallback rate が下がることを確認 +- **(参考、不採用)** ~~C-2 (mistral 出力崩壊起因の場合)~~: Phase B で num_ctx 確定のため scope 外 + +##### ✅ Phase D: Clean dogfood validation (2-3 通常 PR、real pipeline 経由) + +Phase C fix 入りで real pipeline 経由 dogfood。fallback rate が許容範囲 (< 50%) に落ちることを確認。既存 P-1/P-2/P-3 の preview data は Phase C 前の構成なので **不要、新 PR data で代替**。本 phase の通常 PR は Phase d roster から再選定 (Bundle f-1/f-2 / Bundle j-2 / 順位 100-108 docs bundle 等の通常 Tier 1〜3 タスクで代替可能)。 + +##### 🎯 Phase E: 採否判定 + retirement (1 PR、analysis.md 削除を含む) + +- **採用 case**: ADR-038 を「採用」に昇格 + [docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) を削除 (試験運用ガイド役目終了) + 本 analysis.md を削除 + history.md は permanent record として保持判断 +- **却下 case**: cli-finding-classifier crate revert + ADR-038 を「却下」に更新 + Phase d guide 削除 + 本 analysis.md 削除 +- **継続 case**: Phase C が unresolved (Phase B で別問題判明等) なら判定延期 + 本 §「次に何をするか」を再 pivot + +##### Critical path 外 (並行 land 可、本 phase 完了を block しない) + +| Task | Effort | 関連 | +|---|---|---| +| 順位 100-108 docs PR (8 entries の todo registration、bundle 1 PR で消化) | S | Phase A〜C と並行 land 可、commit chain 整理 | +| Bundle c-1 (順位 63+64+67、c-1a/c-1b 分割推奨) | L (M+M+XS、split 推奨) | Phase d とは独立、通常 Tier 1 として後で対応 | +| Bundle j-2 (順位 95+96、`.github/workflows/lint.yml` 新設) | M (S+M) | 独立 | +| Bundle f-1/f-2 (PR #120 feedback) | S+M | 独立 | + +##### Dogfood signal log (旧 PR roster の preview 結果、Phase B/D の比較対象として保持) + +| PR | 構成 | Diff 行 | Latency | findings | fallback_reason | Cumulative fallback | +|---|---|---|---|---|---|---| +| #139 (旧 P-1) | Bundle h + g-2 (docs-only) | 337 | 23s | 0 | `JSON parse error: missing field 'screen_decision'` (line 94) | 1/1 = 100% | +| #140 (旧 P-2) | Bundle j-1 (TOML + Rust regex) | 203 | 46s | 0 | 同 (line 94 column 1) | 2/2 = 100% | +| #141 (旧 P-3) | Bundle d (Rust test only) | 187 | 11s | 0 | 同 (line 1 column 692) | **3/3 = 100%** | + +**観測**: (a) fallback rate 100% が 3 PR 連続 = 既に kill-switch 60% 超過、(b) latency variance (23s/46s/11s) は input size と弱相関 = mistral 内部状態 (cold/warm context) が支配的要因の仮説、(c) すべて同一 fallback_reason = 単一 root cause の可能性大。 + +**Phase d guide §3 kill-switch との関係**: ガイドは「real pipeline 経由で 3/5 fallback 観測 = 停止」と規定。本 preview は cli-finding-classifier 直叩きで pipeline 経由ではないが、3 連続 100% fallback は **厳密 kill-switch 超過に相当する severity**。Phase A〜C で repair しない限り Phase D に進めない判断。 + +**Out-of-roster dropouts**: 旧 P-3 = Bundle g-1 (順位 85+86) は PR #125 で land 済を P-3 着手時に発見 → roster 除外 + stale todo 削除。経緯詳細は `feedback_verify_task_not_already_done.md` 参照。 優先度低の独立 task (Phase d を block しない): - **§8.D**: classify mode の `normalized_issue` 言語制約強化 (low priority、ROI ★) diff --git a/docs/todo-summary.md b/docs/todo-summary.md index 4483400..d1fe26f 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -65,7 +65,6 @@ | 95 | 🔧 Tier 2 | **`docs/todo*.md` preamble file count 自動照合スクリプト (PR #133 T2-#4 採用) ★ Bundle j** | todo6.md | S | なし (PR #133 で todo6.md「六つ」/ todo7.md「七つ」が実 8 ファイルと乖離した実例。todo*.md 分割が今後も繰り返す pattern (todo3 → 4 → 5 → 6 → 7) のため CI 層で自動検証) | | 96 | 🔧 Tier 2 | **Markdown cross-reference validator CI step (PR #133 T2-#3 採用) ★ Bundle j** | todo6.md | M | 順位 10 (ADR-032 PR-broken-link) と方向性が近接、fold-in 検討の余地あり。順位 94 (regex 規約) + 順位 95 (count 照合) と組み合わせて docs/ 整合性の多層検証 | | 97 | 🔧 Tier 2 | **`with_num_ctx(X)` override 値 serialization 検証テスト (PR #136 T2-#1 採用)** | todo6.md | S | なし (PR #136 で追加した builder method の wiring を mockito で seal、Phase d で num_ctx tweak する局面の silent degrade 防止、CodeRabbit が見逃した test gap を post-merge-feedback agent が独立発見) | -| 98 | 🚀 Tier 1 | **`num_ctx` overflow detection — JSON parse error 検知時の context window 診断ログ (PR #137 T1-#1 採用)** | todo6.md | M | なし (PR #136 で誤診 pivot を発生させた blind spot を decisive に塞ぐ runtime hint、`lib-ollama-client` の response validation 層に warn log 追加、`prompt_eval_count` ≈ `num_ctx` cap で truncation を即診断) | | 99 | 💎 Tier 3 | **ADR-038 に PR #138 learning 2 件を追記 (cost-aware 実装層選択 + attention dilution pitfall) (PR #138 T3-#1+#2 採用)** | todo6.md | S | なし (lint_screen が takt facet → Rust stage に pivot した cost 根拠 + Phase b' v2 の diff header full 追加で agreement 75%→50% 33pt 低下した attention dilution 観測の 2 件を ADR に codify、次回 LLM 系 feature 開発時の prior assumption に) | | 100 | 💎 Tier 3 | **`development-workflow.md` に 「同一ファイル複数編集の 1 task 統合」 + 「partial completion + 後続 PR 追補明記」 を追補 (PR #139 T3-#1 採用)** | todo6.md | XS | なし (PR #119/#120/#121 sub-PR 分割 + PR #139 partial completion で systemic に観測された 2 暗黙知を `~/.claude/rules/common/development-workflow.md` に codify、`feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化のため非機械強制でも採用相当) | | 101 | 🚀 Tier 1 | **rule⑧ depth-1 非-docs MD edge case test 追加 (PR #140 T1-#1 採用)** | todo6.md | S | なし (rule⑧ で depth-1 root MD ファイル (例: `./CLAUDE.md`) から `../docs/` 参照が false positive にならないかが未検証、3 ソース (PR diff/prepush/session) で観測された test gap、md_no_docs_relative_* test group に追加) | @@ -73,6 +72,9 @@ | 103 | 🚀 Tier 1 | **lint runner サポートフィールドを code comment で明示化 (PR #140 T1-#3 採用)** | todo6.md | S | なし (`src/hooks-post-tool-linter/src/main.rs` の CustomRule struct 定義近傍に「サポート field: extensions / pattern / severity / message / why、planned: paths」を code comment 追加、TOML コメントのみだと rule author が lint runner 実装を参照する動線が無いため次の設計-実装 gap を構造的に予防) | | 104 | 💎 Tier 3 | **ADR-007 amendment: semantic self-limitation 安全条件 + lint rule 最小テストチェックリスト (PR #140 T3-#1 採用)** | todo6.md | S | なし (rule⑧ で `paths` filter 不在を pattern semantics で代替した判断の rationale を ADR-007 に追記。「semantic self-limitation OK な条件」と「explicit `paths` filter 必須な条件」、lint rule 最小テストチェックリスト = pattern detection / case-insensitive / false positive skip の 3 項目最低化、3 ソース観測) | | 105 | 💎 Tier 3 | **グローバル CLAUDE.md に lint runner サポートフィールド一覧表 (PR #140 T3-#2 採用)** | todo6.md | XS | なし (`~/.claude/CLAUDE.md` に `pattern` / `extensions` / `severity` (planned: `paths`) の field 一覧を表形式で追加、派生プロジェクト (techbook-ledger / auto-review-fix-vc) で rule porting 時の理解統一、順位 103 の code comment と相補) | +| 106 | 🔧 Tier 2 | **self-exclusion test に `path.exists()` ガード + extensions assertion 追加 (PR #141 T2-#1 採用)** | todo6.md | S | なし (PR #141 で land した `no_ephemeral_todo_self_exclusion_invariant_holds_on_deployed_toml` test の false-green ガード、`run_custom_rules` が path 不在で空 Vec を返す silent pass の防止 + extensions list から "toml" 削除時の silent degradation 防止、3 ソース独立指摘で Medium Severity) | +| 107 | 💎 Tier 3 | **`development-workflow.md` に PR #125→#141 anti-pattern 事例補強 (PR #141 T3-#2 採用)** | todo6.md | XS | なし (`~/.claude/rules/common/development-workflow.md` の「タスク完了削除手順」に「マージ後 N 日間 todo.md 残存 → 後続 phase で手動発見」事例を追記、memory `feedback_verify_task_not_already_done` を central rule にも反映、`feedback_todo_no_history` と合わせて「マージ → 即削除」サイクルを強調) | +| 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 ファイル閉鎖を補完) | **戦略**: Tier 1 を 2〜3 セッションで片付け → Tier 2 で ADR-032 の前提 + rate-limit + convergence cost 削減を進める → Tier 3 で ADR-032 を land + ドキュメント整備。Tier 4-5 は cleanup / 外部展開で daily efficiency への直接効果は小さい。 diff --git a/docs/todo5.md b/docs/todo5.md index 6e91783..77e716d 100644 --- a/docs/todo5.md +++ b/docs/todo5.md @@ -203,3 +203,5 @@ --- +--- + diff --git a/docs/todo6.md b/docs/todo6.md index 15d4295..8cae153 100644 --- a/docs/todo6.md +++ b/docs/todo6.md @@ -247,60 +247,6 @@ config.rs + push-runner-config.toml + review-simplicity.md + ADR で family_tag --- -### `num_ctx` overflow detection — JSON parse error 検知時の context window 診断ログ (PR #137 T1-#1 採用) - -> **動機**: PR #136 セッションで「mistral:7b の JSON schema breakdown」を観測した際、Claude は当初「prompt 設計の問題」と誤診し `§8.D v4 prompt 改訂ループ` という名前の誤った means に向かいかけた (実 root cause は `num_ctx` default 4096 超過)。advisor の指摘 + raw Ollama output dump で軌道修正されたが、**runtime layer に診断 hint が出ていれば pivot 時間を短縮できる** ことが判明。`lib-ollama-client` の response validation 層で JSON parse error 検知時に `num_ctx` / `prompt_eval_count` / response length を warn log で auto-emit することで、将来の同型事故 (LLM dogfood 全般で systemic に再発し得る) を decisive に診断できる。 -> -> **本タスクの位置づけ**: PR #137 post-merge-feedback Tier 1 #1 採用 (Severity Medium / Frequency Low / Effort M / Adoption Risk: 派生プロジェクト deploy コストのみ、低)。ADR-038 試験運用配下の infrastructure 強化、Phase d で num_ctx tweak する局面に入る前の安全網としても機能。 -> -> **参照**: `.claude/feedback-reports/137.md` Tier 1 #1、PR #136 で `__dump_raw_ollama.sh` で確認した `prompt_eval_count: 4096` 上限到達 (現在は scratch ファイル削除済、`docs/local-llm-offload-history.md` に経緯記録)、`src/lib-ollama-client/src/lib.rs` の `generate_raw_json` / `OllamaResponse` 構造体 -> -> **実行優先度**: 🚀 **Tier 1** — Effort M。Phase d kickoff 前か実 dogfood 中に整備するのが理想 (dogfood で実際に context overflow を起こした PR があれば即診断できる layer になる)。 - -#### 設計決定 (案) - -- **配置先**: `src/lib-ollama-client/src/lib.rs` の `generate_json::` ヘルパー (型付き parse 失敗時) または raw response 検証層 -- **emit 条件 (案)**: - - **A (主軸)**: `serde_json::from_str` が `missing field` 系 error を返した場合 → `prompt_eval_count` が response の `eval_count` field と比較して context cap に近接していれば warn log - - **B (補助)**: response length が threshold (例: 100 chars) 未満で truncate を疑える場合 - - **C (簡易)**: 常に `prompt_eval_count` / `eval_count` を debug log で emit (low-noise、auto OFF default) -- **emit 内容 (案)**: - ```text - [lib-ollama-client] WARN: Ollama JSON output may be truncated. - parse_error: - prompt_eval_count: (vs num_ctx: ) - eval_count: , response_length: chars - hint: 大規模 prompt は num_ctx を増やすことで解決可能 (with_num_ctx で override)。 - ``` -- **fallback 経路への副作用**: 既存 fallback (block しない、`human_review` + `fallback_reason` を埋める) は維持、log は副次的な diagnostic 出力のみ - -#### 作業計画 - -- [ ] `OllamaResponse` 構造体に `eval_count` / `prompt_eval_count` フィールドを追加 (現状 `response` / `error` のみ deserialize) -- [ ] `generate_json::` ヘルパー (型付き parse) で error 時に上記情報を warn log emit (`log::warn!` または `eprintln!`) -- [ ] threshold ベースの判定ロジック (主軸 A) を実装、補助 B はオプション -- [ ] tests: - - `warn_log_emitted_on_truncated_response_when_prompt_eval_count_high` (mockito + log capture) - - `no_warn_emitted_for_parse_errors_unrelated_to_truncation` (e.g., format-违反 JSON) -- [ ] PR #136 で観測した eval13/15 fixture の `prompt_eval_count: 4096` 状況を dogfood で再現し、log が emit されることを確認 -- [ ] cli-finding-classifier 経由でも log が表面化することを smoke 確認 (push-runner step ログに乗るか) -- [ ] 派生プロジェクト (techbook-ledger / auto-review-fix-vc) 向け deploy 判断 (lib-ollama-client は本リポ専用なら deploy 不要、共有なら別途配布計画) -- [ ] 本 todo6.md エントリを削除 - -#### 完了基準 - -- JSON parse error + context cap 近接の併発時に warn log が emit される -- 既存 fallback 経路 (block しない、graceful degradation) を破壊しない -- LLM dogfood セッションで「context window 起因か prompt 起因か」を log だけで切り分けられる構造 -- 単体テストで diagnostic log の emit 条件 + non-emit 条件の両方を seal - -#### 詰まっている箇所 - -- **派生プロジェクト deploy 戦略**: `lib-ollama-client` が本リポ専用なら deploy なし、共有 crate 化するなら別 repo への copy / git submodule / cargo registry の判断が必要。Phase d 着手判定と合わせて検討 -- **log destination**: `eprintln!` (cli 用途で十分) vs `tracing` / `log` crate 統合 (既存の cli-* との一貫性)。本 lib は現状 ureq + serde_json のみで logging crate なし、初期は `eprintln!` で warn 接頭辞付け、将来必要なら crate 統合という段階導入が自然 - ---- - ### ADR-038 に PR #138 learning 2 件を追記 (cost-aware 実装層選択 + attention dilution pitfall) (PR #138 T3-#1+#2 採用) > **動機**: PR #138 (Phase d kickoff prep) 関連セッションで観測された 2 件の重要 learning が ADR-038 未記録。両者とも次回 LLM/Ollama 系 feature 開発時に再発可能性が高く、ADR に codify することで以下を構造的に防ぐ: @@ -548,3 +494,87 @@ config.rs + push-runner-config.toml + review-simplicity.md + ADR で family_tag #### 詰まっている箇所 - 配置先決定が順位 104 (ADR-007 amendment) の land と依存。順位 104 で field 一覧を inline するなら本タスクはリンク追加のみで済むが、ADR は判断基準中心であれば独立 reference doc が必要 + +--- + +### self-exclusion test に `path.exists()` ガード + extensions assertion 追加 (PR #141 T2-#1 採用) + +> **動機**: PR #141 で land した `no_ephemeral_todo_self_exclusion_invariant_holds_on_deployed_toml` test は、(a) `.claude/custom-lint-rules.toml` の path が存在することと (b) rule の `extensions` に `"toml"` が含まれることに **暗黙的に依存** している。どちらかが将来の変更で壊れると、`run_custom_rules` は空 Vec を返してテスト pass = silent false-green になり、self-exclusion invariant の保護が無効化される。CR Nitpick (PR #141) では (b) のみ指摘 (ユーザー判断で本 PR では不採用)、post-merge-feedback agent は 3 ソース (PR diff / Session / Prepush:simplicity) 独立指摘で (a)+(b) 両方を Medium Severity で再提案。 +> +> **本タスクの位置づけ**: PR #141 post-merge-feedback Tier 2 #1 採用 (Severity Medium / Frequency Low / Effort S / Adoption Risk None)。CR Nitpick の **拡張版** (path.exists ガード追加で完全性向上)。 +> +> **参照**: `.claude/feedback-reports/141.md` Tier 2 #1、`src/hooks-post-tool-linter/src/main.rs` の `no_ephemeral_todo_self_exclusion_invariant_holds_on_deployed_toml` test (PR #141 で追加) +> +> **実行優先度**: 🔧 **Tier 2** — Effort S。2 assertion を test 冒頭に追加。 + +#### 作業計画 + +- [ ] test 冒頭に `assert!(path.exists(), ...)` + `assert!(rule.extensions.contains(&"toml".to_string()), ...)` を追加 +- [ ] エラーメッセージで silent degradation のリスクを説明 +- [ ] cargo test 全 pass を確認 +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- path 不在 / extensions に "toml" 不在のどちらでも test が fail で停止する +- 既存 6 件の rule⑥ test と互換性維持 + +--- + +### `development-workflow.md` に PR #125→#141 anti-pattern 事例補強 (PR #141 T3-#2 採用) + +> **動機**: memory `feedback_verify_task_not_already_done.md` (PR #141 セッションで追加) は session-scoped で「PR #125 → #141 で 4 日間 stale todo 残存 → P-3 起動時に手動発見」事例を含むが、`~/.claude/rules/common/development-workflow.md` の central rule 側には反映されていない。`feedback_todo_no_history.md` と合わせて central 化することで、memory file 閉鎖の structural risk を軽減する。 +> +> **本タスクの位置づけ**: PR #141 post-merge-feedback Tier 3 #2 採用 (Severity Low / Frequency Medium = 2 観測 / Effort XS / Adoption Risk None)。memory rule の central reference への昇格パターン。 +> +> **参照**: `.claude/feedback-reports/141.md` Tier 3 #2、`~/.claude/rules/common/development-workflow.md`、memory `feedback_verify_task_not_already_done.md` / `feedback_todo_no_history.md`、PR #125 / PR #141 + +#### 作業計画 + +- [ ] `~/.claude/rules/common/development-workflow.md` の「タスク完了削除手順」に 2-3 行追記: + - 「マージ後 N 日間 stale entry 残存 → 後続 phase で手動発見」anti-pattern 事例 (PR #125 → #141) + - 「マージ → 即削除」サイクル強調 (memory `feedback_todo_no_history` central 化) + - 「task 着手時に jj log + 既存 test で land 済確認」recovery layer (memory `feedback_verify_task_not_already_done` central 化) +- [ ] central rule から両 memory file への双方向参照を追加 +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- central rule に PR #125→#141 anti-pattern が anchor として記録される +- 新セッションでも両 memory rule の趣旨を central から逆引き可能になる + +--- + +### CLAUDE.md に「Tier 2 偽装検知 + 却下パターン」table (PR #141 T3-#3 採用) + +> **動機**: PR #140 / PR #141 で post-merge-feedback agent が Tier 2 (テスト/自動化) と称した提案を出したが、中身は ルール追加 / checklist 必須化 等の **unenforced rule** で、ユーザー判断で却下相当 (memory `feedback_no_unenforced_rules.md` で codify 済)。memory ファイルは session-scoped で新セッション AI からは見えにくく「Tier 2 = 採用必須」と誤解する構造的リスクがある。グローバル CLAUDE.md に signal + 却下パターン table を可視化し、policy をユーザー可視 + 新セッション AI からも逆引き可能にする。 +> +> **本タスクの位置づけ**: PR #141 post-merge-feedback Tier 3 #3 採用 (Severity Low / Frequency Medium = 複数 session 観測 / Effort S / Adoption Risk None)。memory policy の central reference への昇格パターン。 +> +> **参照**: `.claude/feedback-reports/141.md` Tier 3 #3、`~/.claude/CLAUDE.md`、memory `feedback_no_unenforced_rules.md` (PR #140 / #141 で追記済) + +#### 設計決定 + +- **`feedback_claude_md_link_only.md` との整合**: CLAUDE.md は「リンクのみ」方針。table を inline すると memory rule に違反するため、別 stable doc (`~/.claude/rules/common/post-merge-feedback-policy.md` 等) に table を移し、CLAUDE.md からリンクする運用を推奨 + +#### 検知 signal table 案 + +| Signal | 例 | 判定 | +|---|---|---| +| target field に `*.md` / `test convention` 等 **文書 path** | "lint rule テスト checklist に <条件> を必須化" | ⚠️ Tier 2 偽装疑い | +| description に「**必須化**」「**標準化**」「**チェックリスト追加**」 | "lint rule テストで大文字バリアント必須化" | ⚠️ unenforced rule 強い signal | +| 機械強制 (CI / lint / test 存在検証) なし | "verbal checklist", "guideline 追記" | ❌ 却下相当 | + +#### 作業計画 + +- [ ] 配置先 (新 doc / 既存 doc) を決定 +- [ ] 上記 signal table を新 doc or 既存 doc に追加 +- [ ] CLAUDE.md に link 追加 (memory rule 遵守) +- [ ] 派生プロジェクトへの伝播も検討 +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- 新セッション AI が CLAUDE.md → link → table の動線で Tier 2 偽装判定を逆引き可能になる +- `feedback_claude_md_link_only` 違反なし + diff --git a/src/lib-ollama-client/src/lib.rs b/src/lib-ollama-client/src/lib.rs index 9e71021..6a710e6 100644 --- a/src/lib-ollama-client/src/lib.rs +++ b/src/lib-ollama-client/src/lib.rs @@ -24,19 +24,105 @@ pub use error::OllamaError; pub trait OllamaApi { /// プロンプトを送り、`format: "json"` で得られた JSON 文字列を返す fn generate_raw_json(&self, prompt: &str) -> Result; + + /// プロンプトを送り、JSON 文字列と Ollama 側 metadata を返す。 + /// + /// metadata は `prompt_eval_count` / `eval_count` / `num_ctx` を含み、 + /// JSON parse error 発生時の context overflow 診断に使う ([`generate_json`] 内で消費)。 + /// stub 実装は default の空 metadata で十分 (= diagnostic 不要)。 + fn generate_with_metadata( + &self, + prompt: &str, + ) -> Result<(String, OllamaMetadata), OllamaError> { + let raw = self.generate_raw_json(prompt)?; + Ok((raw, OllamaMetadata::default())) + } +} + +/// Ollama generate API の metadata (context overflow 診断用) +/// +/// `OllamaApi::generate_with_metadata` が返す。`generate_json` が parse error 検知時に +/// stderr へ warn log emit する材料 (PR #136 で「mistral:7b の JSON schema breakdown」を +/// `num_ctx` overflow と誤診せず別アプローチに pivot しかけた事故の構造的予防、 +/// [docs/local-llm-offload-history.md] §A-2 参照)。 +#[derive(Debug, Clone, Default)] +pub struct OllamaMetadata { + /// prompt の token 数 (Ollama API の `prompt_eval_count`) + pub prompt_eval_count: Option, + /// response の token 数 (Ollama API の `eval_count`) + pub eval_count: Option, + /// 呼出時の `num_ctx` setting (overflow 判定の base line) + pub num_ctx: Option, } /// `OllamaApi::generate_raw_json` を呼んで `T` にデコードする無料関数。 /// /// trait に generic を持たせると dyn-compatible でなくなるため、 /// 型付き API は trait の外で提供する。 +/// +/// `T` のデシリアライズに失敗した場合は stderr に warn log を出力する +/// ([`OllamaMetadata`] 参照、context overflow 起因の truncation を decisive に診断する目的)。 pub fn generate_json( api: &dyn OllamaApi, prompt: &str, ) -> Result { - let raw = api.generate_raw_json(prompt)?; - let parsed: T = serde_json::from_str(&raw)?; - Ok(parsed) + let (raw, metadata) = api.generate_with_metadata(prompt)?; + serde_json::from_str::(&raw).map_err(|err| { + emit_overflow_diagnostic(&err, &raw, &metadata); + OllamaError::from(err) + }) +} + +/// `prompt_eval_count` が `num_ctx` の 90% 以上に達している場合に hint 文字列を返す純粋関数。 +/// +/// テスタブルに分離されており、`emit_overflow_diagnostic` はこれを呼び出すだけにする。 +fn overflow_hint(metadata: &OllamaMetadata) -> Option { + let (pec, ctx) = (metadata.prompt_eval_count?, metadata.num_ctx?); + let ratio_pct = (u64::from(pec) * 100) / u64::from(ctx.max(1)); + if ratio_pct >= 90 { + Some(format!( + "prompt_eval_count が num_ctx の {}% に達しています。\ + num_ctx を増やすことで解決可能 (`with_num_ctx` で override)", + ratio_pct + )) + } else { + None + } +} + +/// JSON parse error 検知時の context overflow 診断 log を stderr に emit する。 +/// +/// metadata が `prompt_eval_count` を持ち、かつ `num_ctx` cap の 90% 以上に達している場合は +/// "context overflow 起因の可能性" を明示する hint も含める。 +fn emit_overflow_diagnostic(parse_error: &serde_json::Error, raw: &str, metadata: &OllamaMetadata) { + let prompt_eval = metadata + .prompt_eval_count + .map(|n| n.to_string()) + .unwrap_or_else(|| "unknown".into()); + let eval = metadata + .eval_count + .map(|n| n.to_string()) + .unwrap_or_else(|| "unknown".into()); + let num_ctx_disp = metadata + .num_ctx + .map(|n| n.to_string()) + .unwrap_or_else(|| "unknown".into()); + + eprintln!("[lib-ollama-client] WARN: Ollama JSON output may be truncated."); + eprintln!(" parse_error: {}", parse_error); + eprintln!( + " prompt_eval_count: {} (vs num_ctx: {})", + prompt_eval, num_ctx_disp + ); + eprintln!( + " eval_count: {}, response_length: {} chars", + eval, + raw.len() + ); + + if let Some(hint) = overflow_hint(metadata) { + eprintln!(" hint: {}", hint); + } } /// Ollama 既定の `num_ctx` (2048) は本リポジトリの lint-screen prompt @@ -116,10 +202,14 @@ struct GenerateResponse { response: String, #[serde(default)] error: Option, + #[serde(default)] + prompt_eval_count: Option, + #[serde(default)] + eval_count: Option, } -impl OllamaApi for OllamaClient { - fn generate_raw_json(&self, prompt: &str) -> Result { +impl OllamaClient { + fn request_envelope(&self, prompt: &str) -> Result { let url = format!("{}/api/generate", self.endpoint.trim_end_matches('/')); let body = GenerateRequest { model: &self.model, @@ -148,7 +238,26 @@ impl OllamaApi for OllamaClient { if envelope.response.is_empty() { return Err(OllamaError::EmptyResponse); } - Ok(envelope.response) + Ok(envelope) + } +} + +impl OllamaApi for OllamaClient { + fn generate_raw_json(&self, prompt: &str) -> Result { + Ok(self.request_envelope(prompt)?.response) + } + + fn generate_with_metadata( + &self, + prompt: &str, + ) -> Result<(String, OllamaMetadata), OllamaError> { + let envelope = self.request_envelope(prompt)?; + let metadata = OllamaMetadata { + prompt_eval_count: envelope.prompt_eval_count, + eval_count: envelope.eval_count, + num_ctx: Some(self.num_ctx), + }; + Ok((envelope.response, metadata)) } } @@ -214,7 +323,8 @@ mod tests { #[test] fn returns_api_error_when_ollama_returns_error_field() { let mut server = Server::new(); - let envelope = r#"{"model":"mistral:7b","response":"","error":"model not found","done":true}"#; + let envelope = + r#"{"model":"mistral:7b","response":"","error":"model not found","done":true}"#; server .mock("POST", "/api/generate") .with_status(200) @@ -304,8 +414,8 @@ mod tests { let default_client = OllamaClient::new("http://localhost:11434", "mistral:7b"); assert_eq!(default_client.num_ctx, DEFAULT_NUM_CTX); - let overridden = OllamaClient::new("http://localhost:11434", "mistral:7b") - .with_num_ctx(16384); + let overridden = + OllamaClient::new("http://localhost:11434", "mistral:7b").with_num_ctx(16384); assert_eq!(overridden.num_ctx, 16384); } @@ -338,4 +448,101 @@ mod tests { mock.assert(); } + + #[test] + fn metadata_carries_prompt_eval_count_when_provided() { + let mut server = Server::new(); + let inner_json = r#"{"action":"auto_fix","confidence":0.9}"#; + let envelope = format!( + r#"{{"model":"mistral:7b","response":{},"done":true,"prompt_eval_count":1234,"eval_count":56}}"#, + serde_json::to_string(inner_json).unwrap() + ); + let mock = server + .mock("POST", "/api/generate") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(envelope) + .create(); + + let client = OllamaClient::new(server.url(), "mistral:7b"); + let (_raw, metadata) = client.generate_with_metadata("test prompt").unwrap(); + + assert_eq!(metadata.prompt_eval_count, Some(1234)); + assert_eq!(metadata.eval_count, Some(56)); + assert_eq!(metadata.num_ctx, Some(DEFAULT_NUM_CTX)); + mock.assert(); + } + + #[test] + fn metadata_handles_missing_eval_counts_gracefully() { + let mut server = Server::new(); + let inner_json = r#"{"action":"auto_fix","confidence":0.9}"#; + let envelope = format!( + r#"{{"model":"mistral:7b","response":{},"done":true}}"#, + serde_json::to_string(inner_json).unwrap() + ); + let mock = server + .mock("POST", "/api/generate") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(envelope) + .create(); + + let client = OllamaClient::new(server.url(), "mistral:7b").with_num_ctx(4096); + let (_raw, metadata) = client.generate_with_metadata("test prompt").unwrap(); + + assert_eq!(metadata.prompt_eval_count, None); + assert_eq!(metadata.eval_count, None); + assert_eq!(metadata.num_ctx, Some(4096)); + mock.assert(); + } + + #[test] + fn stub_trait_default_returns_empty_metadata() { + struct StubOllama { + response: String, + } + impl OllamaApi for StubOllama { + fn generate_raw_json(&self, _prompt: &str) -> Result { + Ok(self.response.clone()) + } + } + + let stub = StubOllama { + response: r#"{"action":"informational","confidence":0.1}"#.to_string(), + }; + let (raw, metadata) = stub.generate_with_metadata("prompt").unwrap(); + + assert_eq!(raw, r#"{"action":"informational","confidence":0.1}"#); + assert_eq!(metadata.prompt_eval_count, None); + assert_eq!(metadata.eval_count, None); + assert_eq!(metadata.num_ctx, None); + } + + #[test] + fn overflow_hint_present_when_prompt_eval_count_near_cap() { + let metadata = OllamaMetadata { + prompt_eval_count: Some(7400), + eval_count: Some(50), + num_ctx: Some(8192), + }; + let hint = overflow_hint(&metadata); + assert!(hint.is_some()); + assert!(hint.unwrap().contains("90%")); + } + + #[test] + fn overflow_hint_absent_when_metadata_absent() { + assert!(overflow_hint(&OllamaMetadata::default()).is_none()); + } + + #[test] + fn overflow_hint_absent_below_threshold() { + let metadata = OllamaMetadata { + prompt_eval_count: Some(7000), + eval_count: Some(50), + num_ctx: Some(8192), + }; + assert!(overflow_hint(&metadata).is_none()); + } }