HN 日本語サマリー

← 一覧へ戻る
プログラミング

合成された物語

Synthetic Sagas (scattered-thoughts.net)

6 pointsby ffin0 コメント

要約

著者はAI支援を受けてテキストエディタを書き直しており、AIによるコード生成の効率化と、それに伴うテストやコードレビューの課題について論じています。AIはエッジケースを発見し、指示に疑問を呈することもありますが、依然として「悪魔のランプ」モードで指示を遂行しようとするため、注意深い指示出しが重要だと述べています。また、AI生成コードの品質維持のために、APIスナップショットテストやファジングなどの手法を取り入れています。

全文翻訳

前回から、AIの支援を多く受けながら、私のぎこちない小さなテキストエディタをゼロから書き直す作業を続けています。これは学習プロジェクトとして始まりましたが、最近では新しいエディタの方が古いバージョンよりもぎこちなくなく、機能も豊富なので、実際に使い始めています。インテリジェントなアシスタント 前回とは異なり、今ではAI生成コードを、簡単な手動テストと差分確認の後、そのままコミットすることがよくあります。これが、アーキテクチャとテスト戦略が既に整っていることによるものなのか、それともモデル自体の性能向上によるものなのかははっきりしません。しかし、モデルは確実に向上しています。以前ほど手取り足取り教える必要がなくなり、AIはテストの書き方に関する指示に実際に応じ、私が思いつかなかったようなエッジケースをしばしば発見してくれます。時には、悪い指示に対して反論することさえあります。Opusは「その指示を文字通り実装すると、この明らかな問題が発生するため、実装しませんでした」といった内容を書き、残念ながら正しかったのです。しかし、それは稀です。ほとんどの場合、AIはまだ「悪魔のランプ」モードで、私が間違いを犯したことを指摘するのではなく、あらゆる障害を乗り越えようとします。「Xを実行せよ」と書く代わりに、「Xを実行したいのですが、質問はありますか?」と書くことで、AIが暴走する前に捕捉できることに気づきました。最近の作業の大部分は、月額20ドルのサブスクリプションでClaude CodeのOpus 5によって行われました。私は依然としてPiの方が好きですが、モデルはますます独自のハーネスに対してトレーニングされており、私はテキストファイルとのやり取りに多くの時間を費やしており、ハーネス自体に多くの時間を費やすことを避けています。決定論的なシミュレーション 最大の新しい機能はテストです。コードは2つのクレート、focus-coreとfocusに分割されています。可能な限り多くのロジックがfocus-coreに含まれています。これは入力イベントを受け取り、レンダリングする文字/矩形のリストを返し、外部世界と通信するために&mut dyn IOを使用します。focusクレートには、IOトレイトの実際の」、CLIインターフェース、およびデーモン化ロジックが含まれています。 > scc focus-core/src ─────────────────────────────────────────────────────────────────────────────── 言語 ファイル 行数 空行 コメント コード 複雑度 ─────────────────────────────────────────────────────────────────────────────── Rust 34 13831 1067 1856 10908 1202 ─────────────────────────────────────────────────────────────────────────────── 合計 34 13831 1067 1856 10908 1202 ─────────────────────────────────────────────────────────────────────────────── > scc focus/src ─────────────────────────────────────────────────────────────────────────────── 言語 ファイル 行数 空行 コメント コード 複雑度 ─────────────────────────────────────────────────────────────────────────────── Rust 8 3661 288 720 2653 257 ─────────────────────────────────────────────────────────────────────────────── 合計 8 3661 288 720 2653 257 ─────────────────────────────────────────────────────────────────────────────── その結果、focus-coreのエンドツーエンドテストを書くことが、退屈ではありますが、非常に簡単になりました。モデルはユニットテストを書きたいと強く望むため、テスト対象となる安定した公開インターフェースを与えないと、すべての境界を突き破ろうとします。 #[test] fn dir_picker_ctrl_enter_descends_into_selected_dir() { let (mut app, mut io, window_id) = common::scratch_app(); insert_file(&mut io, "/proj/src/main.rs", ""); common::control_key(&mut app, &mut io, window_id, Key::Character("m")); common::tick(&mut app, &mut io); // ctrl+enter はリストされている最初の(そして唯一の)ディレクトリ (proj/) に下降します。 common::control_key(&mut app, &mut io, window_id, Key::Named(NamedKey::Enter)); common::tick(&mut app, &mut io); // パスエディタは現在 /proj/ を表示しています。 assert_eq!(buffer_text(&app, DIR_PATH), "/proj/"); // リストは現在 src/ を表示しています。 assert!(buffer_text(&app, DIR_LIST).contains("src/")); app.assert_invariants(); } テストはほとんどが場当たり的です。私はほとんど見ません。しかし、回帰を捉え、ロボットに何かを壊したことに気づかせるきっかけになっていることは見ています。また、アプリを開いて数千のランダムなキー入力を送信し、その後app.assert_invariants()を呼び出すファザーもあります。これは場当たり的でカバレッジは低いですが、それでもバグを洗い流します。実際に見なくても、E2Eテストとファジングの組み合わせは、基本的に使用可能なエディタを生成するのに十分なようです。数週間の使用でバグやクラッシュは見ていません。明らかに、テストの労力はリスクと影響によって駆動される必要があります。ユーザーが一人しかおらず、重要なデータを失うリスクがないプロジェクトでは、場当たり的な方法に頼り、実際に遭遇した場合にのみバグを修正する方が、時間効率が良いことがよくあります。実際に重要なものを出荷する場合、アプローチがどのように変わるかはまだわかりません。IOトレイトは安価でひどいソリューションです。より良いシミュレーションの選択肢があればよかったのですが。Rustでは、標準ライブラリにモックを注入する合理的な方法がなく、ほとんどのライブラリはsans-ioスタイルで書かれていないため、独自のモック可能なIOトレイトを使用させることができません。そのため、かなりの量のライブラリ呼び出しは、テストが難しいfocus-coreの外に置かざるを得ません。例えば、デーモン化はテストが難しく、時々回帰します。WASIや何らかのハイパーバイザーを使用して、アンチテーゼライトのようなものですべてをシミュレーション可能な境界内に持ち込むという漠然とした考えがありますが、ロードマップにはすぐにはありません。劣化の回避 コードレビューをどれだけ少なくできるかを見つけようとしています。それは、行動の周りにどのような境界線を描けるかに大きく依存します。例えば、ロボットがPython用の手動トークナイザーを作成しましたが、私はほとんど見る必要がありませんでした。既存のハイライトインターフェースに従っており、制御フローはバッファサイズに対して線形であることが保証されているように見える大きなループであり、すべてのコードは単一のモジュールに含まれています。影響は限定的です。Pythonコードを開いてハイライトが妥当に見えるか確認し、コミットして先に進みます。しかし、ほとんどの変更はそれほど簡単ではありません。バイブコーディングで混乱に陥ることを恐れています。しばしば混乱は少しずつ起こり、各小さな増分は大きなコミットを見たときに完全に明白ではありません。そのため、コードの異なるビューをスナップショットテストとして生成することを試しています。APIスナップショットはすべてのコードを解析し、公開および内部インターフェースのスナップショットを生成します。公開インターフェースの一部を以下に示します。 ## pub mod focus_core::buffer #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug)] pub struct BufferId(/* private fields */); pub struct Buffers { /* private fields */ } impl Buffers { pub fn keys(&self) -> impl Iterator<Item = BufferId> + '_; } pub fn from_file(app: &mut App, io: &mut dyn IO, absolute_path: PathBuf) -> BufferId; impl BufferId { pub fn text(self, app: &App) -> &BStr; } コミットをちらっと見るだけでも、リークする抽象化の導入を捉えるために、インターフェーススナップショットの変更を注意深く読みます。また、focus-core内からのIO関数への偶発的な呼び出しを探すためのカクテルテストもあります。これはかなり場当たり的で、おそらく簡単に回避できます。ビルドスクリプトが問題を引き起こす一部のクレートを無効にし、/dev/randomからのハッシュマップシーディングを許可リストに入れる必要がありますが、何もないよりはずっと良いです。ファザーには、ファザー入力が決してフレームバジェットを使い果たさないことをアサートするアサーションや、漸近線に関する非常にラフな単体テストさえあります。例えば: #[test] fn rust_highlighting_is_linear() { check("speed.rs", include_str!("fixtures/indent.rs")); } /// `sample` を2つのサイズに繰り返したファイルを開き、 /// 大きい方がそのサイズに見合ったコストであることを要求します。 fn check(name: &str, sample: &str) { let small = time_open(name, &repeat(sample, SMALL)); let large = time_open(name, &repeat(sample, LARGE)); let growth = large.as_secs_f64() / small.as_secs_f64(); let rate = (LARGE as f64 / 1e6) / large.as_secs_f64(); let report = format!("{name}: {SMALL} bytes in {small:.2?}, {LARGE} in {large:.2?} ({rate:.1} MB/s)"); assert!( growth <= MAX_GROWTH, "{report}\n{}x the text took {growth:.1}x the time, which is not linea