deterministic-task-protocol リポから miyabi-cli-standalone に統合: - docs/dtp/: PLAYBOOK, PLAN, UML, GIT-RULES, Codex レビュー 3件 - autorun/: Phase 0-8 の TASKS/ASSIGNMENT/GATE + INDEX/HANDOFF/ROLLBACK - project_memory/tasks.json: 全9 Phase の DAG SSOT - skills/: polaris-ops, rust-llm-pitfalls - .codex/instructions.md: Codex 設定 実装は miyabi-core に gate.rs, lock.rs, protocol.rs, store.rs を追加する方針。 既存の dag.rs, github.rs, approval.rs 等は変更不要。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.5 KiB
6.5 KiB
Rust LLM Pitfalls — LLM が Rust を書くとき間違いやすいポイント
LLM(Claude, GPT, Codex)が Rust コードを生成する際に頻出するミス。 エージェントはこのリストを実装前に必ず読み、該当パターンを避けること。
1. 所有権・借用
1.1 ❌ clone() の乱用
LLM は borrow checker エラーを .clone() で解決しようとする。
// ❌ LLM がやりがち
let tasks = self.store.tasks().clone(); // Vec 全体をコピー
for task in &tasks { ... }
// ✅ 正しい: 参照で十分
for task in self.store.tasks() { ... }
ルール: clone() を書く前に「参照で済まないか」を考える。clone() は最後の手段。
1.2 ❌ 可変参照と不変参照の同時使用
// ❌ LLM がやりがち: 同じ構造体から可変と不変を同時に借用
let task = self.store.get_task(id); // &self(不変参照)
self.store.update_task(id, |t| { ... }); // &mut self(可変参照)← コンパイルエラー
// ✅ 正しい: スコープを分ける
let state = {
let task = self.store.get_task(id).unwrap();
task.state // Copy 型なのでスコープ外に持ち出せる
};
self.store.update_task(id, |t| { t.state = state; });
1.3 ❌ String と &str の混同
// ❌ LLM がやりがち
fn find_task(id: String) -> Option<&Task> { ... }
// ✅ 正しい: 入力は &str、保存は String
fn find_task(id: &str) -> Option<&Task> { ... }
2. エラー処理
2.1 ❌ unwrap() の乱用
LLM は Option / Result を unwrap() で解決しようとする。
// ❌ 本番コードで unwrap → panic
let task = store.get_task(id).unwrap();
// ✅ 正しい: ? 演算子 or 明示的エラー
let task = store.get_task(id)
.ok_or_else(|| StoreError::UnknownTask(id.to_string()))?;
ルール: unwrap() はテストコード内のみ許可。本番コードでは ? を使う。
2.2 ❌ エラー型の設計不足
LLM は String をエラー型にしようとする。
// ❌ LLM がやりがち
fn register_task(task: Task) -> Result<(), String> { ... }
// ✅ 正しい: 専用 enum
fn register_task(task: Task) -> Result<(), ProtocolError> { ... }
3. serde / JSON
3.1 ❌ rename_all の不整合
// ❌ snake_case と SCREAMING_SNAKE_CASE が混在
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RiskLevel {
LOW, // → "low" になる(意図と違う?)
MEDIUM,
}
// ✅ 正しい: 意図を明確にする
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] // JSON では "LOW", "MEDIUM"
// または
#[serde(rename_all = "snake_case")] // JSON では "low", "medium"
3.2 ❌ Option フィールドのスキップ忘れ
// ❌ null が JSON に出力される
pub merge_commit: Option<String>, // → "merge_commit": null
// ✅ null を消したいなら
#[serde(skip_serializing_if = "Option::is_none")]
pub merge_commit: Option<String>, // → フィールドごと消える
3.3 ❌ DateTime のシリアライズ形式
// ❌ LLM が独自フォーマットを書こうとする
pub created_at: String, // "2026-04-10 00:00:00" ← 危険
// ✅ 正しい: chrono::DateTime<Utc> + serde
pub created_at: DateTime<Utc>, // ISO 8601 自動
4. 並行性・ファイル操作
4.1 ❌ ファイル操作の非原子性
// ❌ LLM がやりがち: read → modify → write が原子的でない
let content = fs::read_to_string("tasks.json")?;
let mut doc: TasksDocument = serde_json::from_str(&content)?;
doc.tasks.push(new_task);
fs::write("tasks.json", serde_json::to_string_pretty(&doc)?)?;
// ↑ 書き込み中にクラッシュすると tasks.json が壊れる
// ✅ 正しい: atomic rename
let tmp = "tasks.json.tmp";
fs::write(tmp, serde_json::to_string_pretty(&doc)?)?;
fs::rename(tmp, "tasks.json")?; // OS レベルで原子的
4.2 ❌ flock の使い方
// ❌ LLM が flock を忘れる
let content = fs::read_to_string(path)?;
// ↑ 別プロセスが同時に読んでいる可能性
// ✅ 正しい: fs2 で排他ロック
use fs2::FileExt;
let file = File::open(path)?;
file.lock_exclusive()?; // 他プロセスをブロック
let content = fs::read_to_string(path)?;
// ... 操作 ...
file.unlock()?;
4.3 ❌ HashMap の順序依存
// ❌ LLM が HashMap の順序に依存するコードを書く
let locks: HashMap<String, String> = ...;
for (file, task_id) in &locks {
// 順序は保証されない!テストが環境で変わる
}
// ✅ 正しい: BTreeMap を使うか、ソートしてから処理
let mut files: Vec<_> = locks.keys().collect();
files.sort();
5. テスト
5.1 ❌ テストで実ファイルシステムを汚す
// ❌ /tmp に直接書く → テスト並列実行で衝突
fs::write("/tmp/test-tasks.json", "...")?;
// ✅ 正しい: tempfile crate
use tempfile::TempDir;
let dir = TempDir::new()?;
let path = dir.path().join("tasks.json");
5.2 ❌ 時刻依存テスト
// ❌ Utc::now() に依存 → テストが時間で変わる
let lock = TaskLock { locked_at: Utc::now(), ttl_secs: 300, ... };
assert!(!lock.is_expired(Utc::now())); // 瞬間的に通るが不安定
// ✅ 正しい: 固定時刻を注入
let now = Utc.with_ymd_and_hms(2026, 4, 10, 0, 0, 0).unwrap();
let lock = TaskLock { locked_at: now, ttl_secs: 300, ... };
assert!(!lock.is_expired(now + chrono::Duration::seconds(100)));
assert!(lock.is_expired(now + chrono::Duration::seconds(500)));
6. clippy 頻出警告
LLM が生成するコードで clippy が止めるもの:
| 警告 | 原因 | 対処 |
|---|---|---|
needless_pass_by_value |
引数を値で受けて消費しない | & に変更 |
derivable_impls |
手書き Default が derive で済む | #[derive(Default)] |
cast_possible_wrap |
u64 as i64 のオーバーフロー |
.try_into() or .cast_signed() |
unnested_or_patterns |
matches! の書き方が冗長 | パターンをネスト |
redundant_clone |
不要な clone | 削除 |
single_match |
match で1パターンだけ | if let に変更 |
このスキルの使い方
実装前にこのリストを一読し、該当パターンを避ける。 clippy が deny なので、上記の多くはコンパイル時に自動検出される。 ただし ロジックの間違い(unwrap の本番使用、ファイル操作の非原子性)は clippy では検出されない。人間またはレビューエージェントが確認する。