Skip to content

Latest commit

 

History

History
162 lines (130 loc) · 31.5 KB

review-lessons.md

File metadata and controls

162 lines (130 loc) · 31.5 KB

Review Lessons

このファイルは /review-respond で記録した未統合の再発防止パターンの一時置き場です。 /review-respond Phase 6(随時)または /harness-audit(定期点検時)の実施で pj-checklist へ統合され、本ファイルからは削除されます。 過去エントリは git 履歴 (git log -- .claude/review-lessons.md) を参照してください。 エントリ単位は ### <カテゴリ>: ... 見出しで数えます(行数ではない)。

エッジケース: latestReviewsには自分の未提出(PENDING)レビューが混じる (PR #154, 2026-07-06)

  • 問題: 「レビュー提出済みユーザーのみ」という仕様をlatestReviews由来であることだけで担保していたが、gh pr view --json latestReviewsにはviewer自身の未提出(state == "PENDING")レビューが含まれることがあり、未提出のレビュアーが候補に混じりうる
  • 対策: latestReviewsを「提出済みレビュー」として扱う処理ではstate ~= "PENDING"を明示的に確認する。docstringに書いた入力データの前提は、コード側の条件として保証する
  • 該当箇所: lua/fude/ui/format.lua

コード品質: 純粋モジュールでのvim.NIL安全なネスト参照 (PR #154, 2026-07-06)

  • 問題: gh APIレスポンス由来のネストフィールド(review.author.login等)をx and x.yで参照していたが、JSON nullはvim.NIL(truthyなuserdata)にデコードされるためindexエラーになりうる。util.is_nullはvim.NILを参照するため、純粋性チェック対象モジュール(*/format.lua・*/data.lua)では使えない
  • 対策: 純粋モジュールではtype(x) == "table" and x.y or nilでガードする。外部APIレスポンスのネスト参照を書くとき、既存の同パターン箇所(同ファイル内の類似関数)も同時に確認する
  • 該当箇所: lua/fude/ui/format.lua, lua/fude/overview.lua

コード品質: docstring制約は実装で強制する(空Lua tableのJSONエンコード) (PR #154, 2026-07-06)

  • 問題: 「空配列は渡さないこと」をdocstringの注意書きだけで担保していた。空のLua tableはvim.json.encodeでJSONオブジェクト({})にエンコードされるため、破ると不正なペイロードをAPIに送る
  • 対策: 呼び出し側の規約に依存せず、関数冒頭で空入力を弾いてエラーcallbackする。配列をJSONエンコードする関数では空テーブルがオブジェクトになるLuaの仕様を常に考慮する
  • 該当箇所: lua/fude/gh.lua
  • 再発 (PR #173, 2026-09-03): 「#を含むパスは非対応」をdoc/fude.txtのLimitationsに書くだけで、コード側は#入りパスをそのまま抽出・添付していた(ghが#をalt区切りとして分断し誤添付になる)。「非対応」と文書化した入力は、コードでも検出して安全側(未処理のまま維持・スキップ)に倒すこと

エッジケース: 集約判定は部分集合ではなく論理的対象の全体に評価する (PR #163, 2026-07-23)

  • 問題: inline modeでresolvedコメントのboxを隠す際、行末フォールバックの[resolved]判定を「隠した部分集合(hidden_resolved)」だけで行っていたため、同一行に未解決コメントが混在してもall_comments_resolvedが常にtrueになり、誤って[resolved]が表示された。「この行は全部resolvedか」という集約述語に行の一部だけを渡して意味が変わっていた
  • 対策: 集約述語(all_X / every_X 等)は、その述語が論理的に対象とする集合の全体(行の全コメント等)に対して評価する。フィルタ後の部分集合を渡すと述語の意味が壊れる。あわせて、要素を振り分けるフィルタ条件と集約述語は同じフィールド(is_resolved)で判定し、食い違いを構造的に排除する
  • 該当箇所: lua/fude/ui/extmarks.lua

テスト: 実クロックと比較されるfixture日付の時限爆弾 (PR #155, 2026-07-06)

  • 問題: retention pruneを通る実ロードパスのテストで、fixtureのsaved_atをハードコードした過去日付にしていたため、日付経過でretention window(30日)から外れテストが壊れた。動的なos.date()(現在時刻)への修正も「実行時刻依存で再現性が落ちる」とレビュー指摘を受けた
  • 対策: 実時刻(os.time())と比較される経路を通るfixtureのタイムスタンプは、固定の十分未来の日付(例: 2126-01-01T00:00:00Z)を使う。過去日付のハードコードは時限爆弾、現在時刻の動的生成は再現性低下。時刻を注入できる純粋関数(prune(t, now, days)等)のテストは固定nowを渡して書く
  • 該当箇所: tests/fude/drafts_spec.lua

コード品質: 近似パーサに記法を足すときは暗黙の制約と開閉の非対称を個別に確認する (PR #173, 2026-09-04)

  • 問題: (1) angle-bracket入力形式(](<file://...>))を後から追加した際、bare形式ではパターン構文上通らない)がangle形式では通るようになり、出力正規化と組み合わさって「ghが書き換えられない参照」を生んだ。(2) フェンスの開き判定(info string可)をそのまま閉じ判定にも使ったため、フェンス内の```luaのような行で誤って閉じた
  • 対策: 同じデータに対する記法を追加するとき、既存形式が構文上(暗黙に)排除していた文字・ケースを列挙し、新形式でも同じ制約が成立するかを確認する(「パターンが通さないから安全」は形式追加で崩れる)。開始と終了で許される構文が異なる構造(fence、heredoc等)は閉じ判定を仕様に沿って別に書く
  • 該当箇所: lua/fude/pr.lua

コード品質: 依存プリミティブの保証と重複する検証コード (PR #169, 2026-07-23)

  • 問題: シェルスクリプトでjq -cによるJSON生成後、生成した行が実際にファイル末尾に追記されたかをtail -n 1で再読み込みして比較検証していたが、(1) jq -cは出力が単一行のcompact JSONになることを既に保証しており、(2) 追記コマンド自体の失敗はset -euが既に検知するため、この検証は実質どの失敗も追加で捕捉していなかった。むしろ同時書き込み(人間側の操作等)が割り込むと、追記自体は成功しているのに検証だけ失敗する誤検知を生んでいた
  • 対策: 検証コードを書く前に「この検証は、依存しているプリミティブ(jqのフォーマット保証、set -eのfail-fast等)が既にカバーしていない失敗モードを捕捉しているか」を確認する。捕捉対象が存在しないなら、検証の精度を上げる(マッチ窓を広げる等)のではなく検証自体を削除する
  • 該当箇所: contrib/skills/fude-watch/fude-watch-reply.sh

コード品質: Luaのand-orイディオムはnilを返せない。直すときは値が兼ねる副作用も見る (PR #179, 2026-09-14)

  • 問題: cond and nil or fallback / is_null(x) and nil or x は、中間値がnil/falseだと必ずfallback側に落ちるため「条件成立時にnilを返す」意図を表現できない。gh APIレスポンスのnull正規化(新規)とsuggest入力のcursor_pos分岐(既存)の2箇所で実バグ化しており、後者は数ヶ月間気づかれなかった。さらにcursor_posはstopinsertのトリガーも兼ねていたため、「値」だけ直してnilを渡すとinsertモード開始という隠れた挙動変化が起きた
  • 対策: 三項演算子のつもりでand-orを書くとき、真側の値がnil/falseになり得るなら明示的なif文かutil.null_to(v, default)を使う。レビュー時はand nil or・and false orをGrepして機械的に検出できる。パラメータの値を変える前に、受け側でそのパラメータがnil/非nilでゲートしている副作用(モード、フォーカス等)を確認し、テストもその観点で書く
  • 該当箇所: lua/fude/gh.lua, lua/fude/comments.lua, lua/fude/ui.lua, lua/fude/util.lua (null_to)

エッジケース: 非同期submit成功後のdraft削除が送信中の保存を消す (PR #179, 2026-09-14)

  • 問題: 「API成功後に削除」の原則を守っていても、削除対象(draft等の永続データ)がリクエスト往復中にユーザー操作で更新され得る場合、成功callbackの無条件削除が新しい保存を消す。PR editフロート・comment browser下ペインのように入力UIがリクエスト中も開いたまま操作可能な設計では特に到達しやすい
  • 対策: 非同期成功後にユーザーデータを削除するときは「送信時点の対象と同一か」を確認する。同一クロージャ内で完結するなら保存ハンドラでフラグを立てる方式(内容一致のエッジも塞げる)、クロージャをまたぐならsnapshot比較(drafts.remove_if_unchanged)を使う。snapshotは本文ではなく保存ごとに変わるrevision(drafts.revision)にする。本文比較だと同じ本文の再保存を「変更なし」と見なして消す(PR #236)。修正時は同パターンの全サイト(drafts.remove等の成功後削除)をGrepで列挙し、UIが開いたままのサイトを優先する
  • 該当箇所: lua/fude/pr.lua, lua/fude/ui/comment_browser.lua, lua/fude/drafts.lua

ドキュメント: 非nilデフォルトのオプションを「nilで無効化」と案内していた (PR #176, 2026-09-09)

  • 問題: diffoptのコメントとhelpで「nil to keep user's default」と案内していたが、setup()はvim.tbl_deep_extend("force", defaults, user_opts)でマージするため、setup({ diffopt = nil })はキー自体が落ちてデフォルトが適用される。デフォルトが非nilのオプションではnilによる無効化は不可能で、案内どおりに設定したユーザーは無効化できない
  • 対策: 「無効化の方法」を書くときは、デフォルト値がnilか非nilかを確認する。非nilデフォルト(table/文字列/数値)のオプションでは{}/falseなど実際にマージ後に残る値で案内し、実装側の分岐(if opts.x then / ipairs(opts.x))がその値で意図どおりスキップされることを確認する。同じ既定値ブロックがREADME/doc/config.luaに複製されているので、文言変更時は3箇所をgrepする
  • 該当箇所: lua/fude/config.lua, doc/fude.txt, README.md

エッジケース: 操作後の対象同一性を曖昧な再探索結果で判定しない (PR #187, 2026-09-18)

  • 問題: 正規化したパスでバッファを探索し、その結果のバッファ番号をオープン後のウィンドウと比較していた。同じ実体ファイルを指す別名バッファが複数あると、探索が別のバッファを返して正常な処理を中断し、view復元やセンタリングを省略しうる
  • 対策: パス探索では完全一致を別名一致より優先する。操作後の対象確認では再探索せず、ウィンドウが実際に表示しているバッファの正規化パスを要求した対象と直接比較する。別名が複数あるケースを回帰テストに含める
  • 該当箇所: lua/fude/files.lua

エッジケース: 非同期で確定する状態に対するno-op判定 (PR #180, 2026-09-16)

  • 問題: scope切り替えのno-op判定(同一scopeなら何もしない)がstate.scopeを見ていたが、この値は非同期のAPIコールバックが着いて初めて更新される。切り替え中に別のscopeを選ぶと、判定は古い値で行われ、保留中のリクエストを無効化しないまま早期returnし、後着のコールバックが最新の選択と異なるscopeを適用して完了コールバックまで発火した
  • 対策: 「現在の状態」と比較する判定を書くとき、その状態がいつ確定するかを確認する。確定が非同期なら、in-flightの遷移先を別に追跡し(pending_switch等)、判定はそれと比較する。同一遷移先の再選択は保留中リクエストへ重複排除、別遷移先の選択は世代カウンタで保留中を無効化、という形で「最新の選択が必ず勝つ」を成立させる
  • 該当箇所: lua/fude/scope.lua

堅牢性: 非同期処理中に走る同期的な副作用の後始末 (PR #180, 2026-09-16)

  • 問題: apply_commit_scopeはgit checkout(同期)を実行してからAPIを叩くが、state.scopeがcommitになるのは成功コールバック内。この間にstop()すると、HEAD復元の条件がstate.scope == "commit"だったため復元されず、レビュー停止後も作業ツリーがdetached HEADのまま残った
  • 対策: 非同期リクエストの前に同期的な副作用(checkout、ファイル作成等)を実行する関数では、その副作用を「リクエストが所有するリソース」として扱い、teardown側から参照できる形で公開する(has_pending_*())。あわせて停止時は保留中リクエストを明示的に無効化し、遅れて返るコールバックがteardown後に動かないようにする
  • 該当箇所: lua/fude/scope.lua, lua/fude/init.lua

コード品質: プラグイン内部のフォーカス移動をユーザー操作と誤認する (PR #180, 2026-09-16)

  • 問題: 自動オープンに「カーソルがパネルを離れていたら実行しない」というstalenessガードを入れたが、同じ処理内で呼ぶrefresh_preview(open_previewがsource windowへフォーカスを移す)が先に走るため、ユーザーが何もしていなくてもガードに引っかかり機能が常に沈黙した。さらに復元処理側も、close_previewで破棄済みのwindow handleと比較していたため、preview内にいたユーザーを別windowへ落とした
  • 対策: 「ユーザーが離れたか」を判定するガードを入れるときは、同じフローでプラグイン自身がフォーカスを移していないかを確認する。内部でフォーカスを移す関数は呼び出し元のフォーカスを復元して閉じる(破棄されるwindowが呼び出し元の場合は再生成後の対応windowへ移す)
  • 該当箇所: lua/fude/scope.lua, lua/fude/preview.lua, lua/fude/ui/sidepanel.lua

コード品質: 後から追加したデフォルトが既存ユーザーの明示設定を上書きする (PR #181, 2026-09-16)

  • 問題: sidepanelにnext_entry = "j" / prev_entry = "k"のデフォルトを追加したが、キーマップ登録が後勝ちだったため、既にselect = "j"等を設定していたユーザーの明示設定が新デフォルトに黙って奪われる状態だった。さらに先勝ちに変えた後は、登録順が優先順位の仕様になったにもかかわらず、実際の登録順がドキュメントの列挙順と一致していなかった
  • 対策: 既存の設定キー集合に新しいデフォルト値を追加するときは、既存設定との衝突時にどちらが勝つかを決めて明示する(原則は既存設定優先)。優先順位がコードの実行順で決まる設計にしたら、その順序をドキュメントの列挙順と一致させ、衝突ケースのテストで固定する
  • 該当箇所: lua/fude/ui/sidepanel.lua, doc/fude.txt

エッジケース: 外部コマンドの非ゼロ終了を一律「対象なし」として扱う (PR #191, 2026-09-25)

  • 問題: gh pr view <branch>はPRが無いときも認証切れ・ネットワーク障害のときも非ゼロで終了する。全エラーを「PRなし」に変換したため、ユーザーに「open PRがない」と誤った原因を伝えていた
  • 対策: 「見つからない」をエラー終了で表すCLIを呼ぶときは、not-foundのメッセージ(no pull requests found等)だけを「対象なし」として扱い、それ以外はエラー内容を呼び出し元へ返して別の通知にする。判定はis_*_errorのような関数に切り出してテストする
  • 該当箇所: lua/fude/gh.lua, lua/fude/pr.lua

堅牢性: 同期パスで候補数に比例してプロセスを起動する (PR #191, 2026-09-25)

  • 問題: picker表示前の同期処理で、候補branchごとにgit rev-list --countをvim.system():wait()で起動していた。候補が多いリポジトリではUIが目に見えて止まる
  • 対策: UI表示前に走る同期処理では、外部コマンドの起動回数が入力件数に比例しないようにする。gitならgit log --decorate-refsやfor-each-ref --formatのように1回で必要な情報(所属と順序)を取れる呼び方を先に探す
  • 該当箇所: lua/fude/diff.lua

テスト: 実gitリポジトリを作るテストで作者情報をグローバル設定に依存する (PR #191, 2026-09-25)

  • 問題: 一時repoでgit commitには-c user.name=...を付けたが、git commit-treeには付け忘れた。ローカルはグローバル設定で通り、作者情報の無いCIだけAuthor identity unknownで落ちた
  • 対策: テストで一時repoを作ったら、直後にgit config user.name/user.emailをrepoローカルに設定する(コマンドごとの-c指定に頼らない)。ローカル確認はHOMEを空ディレクトリに向けてグローバル設定を切った状態でも行う
  • 該当箇所: tests/fude/diff_spec.lua

コード品質: 自由形式の文字列から識別子を「走査」して照合する (PR #206, 2026-10-03)

  • 問題: keymapのrhsからユーザーコマンド名を探す際、(1) rhs全体を小文字化して比較したため実行時にE492になる<cmd>fudereviewdiff<cr>まで一致し、(2) rhs中のどこに現れても直前が:/>/|なら一致としたためecho ':FudeReviewDiff'やlua print(">FudeReviewDiff")のような文字列中の出現まで一致した。「直前の記号を見る」を広げるほど引用文字列・コメントの除外(=Exパーサ)が必要になり収束しなかった
  • 対策: 大小文字を区別して解釈される識別子(コマンド名・パス等)は完全一致で探し、case-insensitiveは記法(<Cmd>等)に限定する。文字列中の出現を排除したいときは「どこかに現れる」走査ではなく、構文上の決まった位置(rhs先頭のラッパー直後など)にアンカーして照合し、対象外になる形(|連結後など)はdocとテストで明示する。テストには小文字化した識別子・引用文字列中の出現が一致しないケースを含める
  • 該当箇所: lua/fude/palette.lua

エッジケース: 最小サイズの下限が利用可能サイズの上限を上回る

  • 問題: ウィンドウサイズを「希望サイズと最小値のmax」→「利用可能サイズとのmin」で求める際、利用可能サイズ側にも下限(例: 20桁/6行)を置いていたため、それより小さい端末では画面を超えるサイズを返した。「負や0にならない」ことだけ確認していて、「画面に収まる」ことを境界値で確認していなかった
  • 対策: 下限は「通常時の最小サイズ」と「利用可能サイズの下限(1)」を分け、最後に適用するclampは必ず利用可能サイズ側にする。テストはcolumns/linesが最小サイズ未満のケースを含め、返り値が利用可能サイズ以下かつ1以上であることを確認する
  • 該当箇所: lua/fude/palette.lua

ドキュメント: 複数の経路で分岐する機能を無条件の機能説明で謳う (PR #206, 2026-10-03)

  • 問題: ピッカーがTelescope/snacks/vim.ui.selectに分岐するのに、README・helpは「説明で検索できる」と無条件に書いていた。素のvim.ui.selectは番号選択で検索できないため、fallback経路ではドキュメントの約束を満たさなかった
  • 対策: 実装経路(picker種別・provider有無・モード)で振る舞いが変わる機能は、ドキュメント側で「どの経路で何ができるか」を経路ごとに書く。機能説明を書くときは、fallback経路でもその文が真かを確認する
  • 該当箇所: README.md, doc/fude.txt

アーキテクチャ: review_modeごとの並行実装で、ガード・状態・失敗方針が片側にしか無い (PR #207, 2026-10-03)

  • 問題: local commit scopeを足した際、(1) 読み取り専用ガードをcomments.luaのfacadeにだけ置いたため、backend(local_sync)を直接呼ぶcomment browserからは書き込めた。(2) apply_gitsigns_base_for_bufferはGitHub側のstate.scopeしか見ておらず、同じ概念をlocal側はstate.local_session.scopeに持つため分岐に入らなかった。(3) init.stop()はHEAD復元失敗でセッションを維持する方針だったが、local/session.stop()は復元失敗後もteardownを続けてdetached HEADに取り残した
  • 対策: 横断的な制約(read-only等)は呼び出し経路に依存しないbackend層で強制し、facadeのガードは通知用に留める。GitHub/localで同じ概念を別フィールドに持つ場合、共通helperが両方を見ているかstate.review_modeで確認する。片側に既に存在する失敗時の方針(復元失敗→中断・維持)は、もう片側に同種の処理を足すとき必ず照合する
  • 再発 (PR #236, 2026-10-10): ローカルbackend(local_sync)はcallbackを同期で呼び、その中で再描画まで済ませる。GitHub backendの非同期callbackを前提に「callback→vim.scheduleで下書き削除→直後にrefresh()」と書いた箇所は、ローカルでは削除前に再描画が走り✎draftが残った。同じ呼び出し元を両backendで共有するときは、callbackが同期か非同期かで後始末と再描画の順序が変わらないか確認し、再描画は後始末の後に置く
  • 該当箇所: lua/fude/comments/local_sync.lua, lua/fude/init.lua, lua/fude/local/session.lua, lua/fude/ui/comment_browser.lua

堅牢性: HEADを動かす操作の前提条件と復旧情報は、全経路・全方向で揃える (PR #207, 2026-10-03)

  • 問題: commit scopeのcheckoutについてCopilotが7ラウンドにわたり同系統の穴を指摘した。(1) clean判定が初回進入時だけで、commit→commitや「戻る」方向では未検査だった。(2) git statusが見ない状態(未保存バッファ、未送信のコメント入力、別tabの入力float、内側worktreeのバッファの誤判定)を見落としていた。(3) 復旧用pointerをcheckoutの後に書いていたため、その間のクラッシュで復帰不能だった。pointerの書き込み失敗を無視して進んでいた。復元後にstaleなcommit pointerが残り、意図的なdetachを誤認した
  • 対策: 外部状態を変える操作(checkout等)は、進入・遷移・復帰の全方向で同じ前提条件チェックを通す。チェック対象はgitの状態だけでなくNeovim側の状態(modifiedバッファ、入力UIの未送信テキスト)も列挙し、探索範囲(全tab、所有worktreeの最深一致)を明示する。復旧情報は副作用の前に永続化し、書けなければ中止する(write-ahead)。復帰に成功したら復旧情報を更新し、更新できなければ削除する(staleな復旧情報は無いより悪い)。唯一の例外は終了時(VimLeavePre)で、detached HEADに取り残すよりは警告して強行する
  • 該当箇所: lua/fude/local/session.lua, lua/fude/ui.lua

堅牢性: セッション同一性ガードをエラー分岐より後に置いていた (PR #212, 2026-10-04)

  • 問題: gh応答コールバックでif err then vim.notify ... return endを先に書き、config.state ~= captured_stateのチェックをその後に置いていた。停止・再開後に届いた失敗応答がガードに到達せず、古いセッションのエラーが現行セッションの通知として表示された。成功経路だけを「古い応答を捨てる」対象と考えていた
  • 対策: 非同期コールバックの同一性ガードは、エラー分岐を含むあらゆる分岐より前、コールバックの先頭に置く。「成功時のstate更新を守る」だけでなく「失敗通知も古いセッションのもの」であることを前提にする。テストはreset_state()後に成功応答と失敗応答の両方を流し、通知・on_done・stateのいずれも動かないことを確認する。既存の同種サイト(stack.luaのget_pr_stackコールバック、scope.luaのis_current_request)は先頭チェックなので、それに揃える
  • 該当箇所: lua/fude/files.lua

堅牢性: 非同期checkout中のレビュー再入と、復元失敗後の続行 (PR #203, 2026-10-01)

  • 問題: :FudeReviewStackSwitchはinit.stop()→gh pr checkout(非同期)→callbackでinit.start()という流れだったが、(1) checkout中にユーザーが別のレビューを開始できたため、callback時点のactiveだけ見る判定では新レビューが旧ブランチのstateのまま残り、start()が二重に走り得た。(2) 元ブランチへの復元checkoutが失敗してもchecktime→start()を続行していたため、HEADが中途半端な状態で別PRのレビューを開始し得た
  • 対策: 外部状態(HEAD)を非同期に書き換える処理の間は、競合する入口(init.start()・local/session.start())を共有フラグで明示的に拒否する(単一の切替しか走らない設計ならgenerationではなくフラグで足りる)。復元に失敗したら処理を続行せず、レビューを停止したまま手動復旧の手順(checkoutすべきref)をERRORで案内する。「中断・維持」の方針は後続のHEAD操作(#207 commit scope)でも照合する
  • 該当箇所: lua/fude/stack.lua

エッジケース: prunableなworktreeもgitはブランチを予約している (PR #203, 2026-10-01)

  • 問題: git worktree list --porcelainのprunableエントリ(ディレクトリが消えたworktree)をパース時に捨てていたため、そのブランチは「どこにもcheckoutされていない」と判定されgh pr checkoutに進んだが、gitはmetadataが残る限りそのブランチを別worktreeでcheckout済みとして予約するためcheckoutは失敗した。「ディレクトリ不在ならstop前に拒否」という設計も、エントリを捨てた時点で到達不能になっていた
  • 対策: 外部ツールの一覧出力を「使えないものを除外」する形でパースするときは、除外した要素が後続の操作で別の失敗(予約・ロック等)を引き起こさないかを確認する。失敗するなら除外せず保持し、事前検査で明示的に拒否して復旧手順(git worktree prune)を案内する
  • 該当箇所: lua/fude/diff.lua, lua/fude/stack.lua

コード品質: 数値IDの文字列比較は桁数の境界で順序が逆転する (PR #210, 2026-10-03)

  • 問題: 同じcreated_atを持つコメントのタイブレークをid同士の文字列比較で行っていたため、999999999と1000000000のように桁数が変わる境界で後者が先に並んだ。ブラウザはこの順序を保持し、find_editable_commentが末尾から探すため、e/dが最新ではなく古いコメントを対象にした
  • 対策: gh APIのidはnumberで届くので、ソートキーに使うときは数値として比較する(文字列と数値が混在し得るなら両方数値のときだけ数値比較し、それ以外は文字列比較にフォールバックする)。同時刻・異なる桁数のidを並べる回帰テストを置き、順序だけでなくそれに依存する選択結果(編集対象)まで固定する
  • 該当箇所: lua/fude/comments/data.lua

ドキュメント: 既存コードを「先例」として挙げるときはgrep結果ではなく構造を読んで確認する (PR #213, 2026-10-04)

  • 問題: 「同一性ガードはコールバック先頭に置く」というルールの先例としてinit.reloadを挙げたが、実際には各取得コールバックの更新条件内と完了バリア後で確認する構造で、ルールの参照先と説明が食い違っていた。captured_stateをgrepしてヒットしたファイルを、構造を読まずに先例として列挙していた
  • 対策: ルールの根拠として既存コードを名指しするときは、そのコードがルールの要件(位置・順序・条件)を本当に満たしているかを該当関数を読んで確認する。grepで「同じ識別子を使っている」ことと「同じ規則に従っている」ことは別で、満たさない例を挙げるとルール自体の信頼性を下げる
  • 該当箇所: .claude/review-lessons.md

ドキュメント: 入力元が生成しない分岐を根拠に機能を文書化した (PR #233, 2026-10-10)

  • 問題: parse_name_statusのC(copy)分岐があることを根拠に「copied fileも旧パスと比較する」と書いたが、実際の入力元diff.get_name_statusはgit diff -Mでrenameしか検出しないため、local modeではcopyがaddedになり分岐に到達しなかった
  • 対策: パーサーの分岐を根拠に挙動を文書化するときは、呼び出し元のコマンドやAPIがその形式を実際に出力するか(gitのフラグ、APIのstatus値)まで遡って確認する。到達しない分岐はmode限定で書くか、記載から外す
  • 該当箇所: lua/fude/local/session.lua, doc/fude.txt

エッジケース: カーソル連動の表示が「同じ行ならskip」のキャッシュで再描画に追従しない (PR #237, 2026-10-10)

  • 問題: 複数行コメントの範囲強調をupdate_inline_hint(CursorMovedで呼ばれ、同じbuffer/lineなら早期return)に載せたため、カーソルを動かさないままreloadや再anchorでcomment_mapが変わると、削除・移動された範囲の強調と開始マーカーが次のカーソル移動まで残った
  • 対策: カーソル位置をキーに「表示済みならskip」するextmarkを足すときは、そのデータを書き換える描画経路(refresh_extmarks等)からも無効化・再構築する。他windowをnvim_win_callで描画する経路があるので、再構築は表示中のバッファに限り、カーソルのあるwindowから表示を移さない
  • 該当箇所: lua/fude/ui/extmarks.lua