派生フォークを「構成一致」でなく「振る舞い一致」で同期する
本体から勤怠だけを切り出した派生フォークを同期する話。構成の一致を捨て、振る舞いの同等性を実体で判定するという設計判断の記録。
部分集合として生まれたフォーク
NGWから、勤怠管理の機能だけを切り出して別デプロイにした。いわゆる派生フォークである。両方ともgit管理下にあるが、構成は意図して揃えていない。アプリのルート位置もincludeの相対的な深さも違うし、フォーク側は新しめの流儀を採っている。ファイル冒頭の declare(strict_types=1)、POST/GETが空のときにCookieへフォールバックする入力取得。機能としては本体の部分集合だ。
厄介なのは、乖離が双方向だという点である。本体は機能追加で先行し、フォークは一部の防御的な修正で先行している。どちらかが常に上流、という単純な関係になっていない。ここで「同期」をどう定義するかが、その後の運用コストをまるごと決めてしまう。
同期のゴールは「同じコード」ではない
最初に合意したのは、ディレクトリ構成やファイルの一致は目指さない、ということだった。目指すのは機能の同等性、つまり振る舞いの一致である。共有ロジックに手を入れたら両方へ反映する。ただしファイルを丸ごとコピーするのは禁じ手にした。includeパスが壊れるし、何よりフォーク独自の改善が上書きで消えてしまう。
だから移植はロジック単位でやる。関数の中身、条件分岐、SQLの一節。それを相手側の作法に合わせて置き直す。フォークにしか無い改善——ゼロ除算のガードや、未入力時の値補完——は温存が絶対条件だ。移植したあとは各ファイルを構文チェックにかけ、差分の統計を見て「独自コードを消していないか」、つまり削除行がゼロであることを確認する。追加はあってよいが、削除は事故のサインとして扱う。
「無い」を名前で判定しない
もうひとつ、痛い目を見て学んだ罠がある。「本体には有るがフォークには無い機能」を、関数名の照合だけで判定してはいけない。
同じ機能が、別の名前・別のファイルで両方に実装されていることがある。しかも内部の作り方まで違う。片方はレコードを直接DELETEし、もう片方は取り消しAPIを経由して該当カラムをNULLにする。振る舞いとしては等価でも、名前で突き合わせれば「フォークには無い」と誤判定する。その誤判定のまま移植すれば、二重実装になるか、既存の正しい挙動を潰す退行になる。
だから移植の前には、必ず機能の実体を確かめる。名前ではなく、何が起きるかで等価性を見る。grepで名前が引っかからないことは、機能が無いことの証明にはならない。
AIとの協働作業での学び
フォークの同期とは、コードを同じにすることではなく、振る舞いを同じにすることだ。この一語の差が実務ではとても大きい。構成の相違はコストではなく前提として受け入れ、差異は差異のまま管理台帳に乗せる。そして等価かどうかは、名前という表層ではなく挙動という実体で判定する。
分岐したコードベースを二つ抱える人へ。揃えるべきは見た目ではない。ユーザーから見て同じ答えが返るか、それだけである。