タグ: gui

  • Spec-kitの活用

    mcts-genとpy-chessboardjsにおけるSpec-kitの活用と開発ワークフロー改善の記録

    この記事では、私が最近行った2つのプロジェクトmcts-genpy-chessboardjsでの開発記録を基に、AIを活用したspec-kitツールの使い方、GitHub Actionsを用いたCI/CDワークフローの構築、そしてPyPIのTrusted Publisher設定について解説します。


    1. spec-kitのアップデートと古い設定ファイルの整理

    プロジェクトmcts-genakuroiwa/mcts-gen)において、spec-kitのバージョン更新を行いました。この際、古いファイルの削除に手間取りました。また、仮想環境管理ツールuvxを用いた初期インストール手順が、恒久的なツールの利点を享受できない原因となりました。

    🚨 古いバージョンからの移行時の注意点

    以前のバージョンでspecify initを実行していたため、新しいバージョンのspec-kitを導入する際、古いコマンド定義ファイルが残ってしまいました。

    現在のgemini-cliでspec-kitのコマンドは全て/speckit.から始まりますが、古いバージョンでは異なっていました。

    【新しいspec-kitのインストール】

    仮想環境をアクティベートした後、uv tool installで最新版を取得します。

    source .venv/bin/activate
    uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git

    【内部コマンドの更新】

    プロジェクトのルートで以下のコマンドを実行し、gemini-cliの内部コマンドを更新します。

    specify init --here

    これにより、新しいコマンドファイルが.gemini/commands/以下に作成されます。

    【新規ファイルの確認】

    git statusで確認すると、以下のような新しいファイルが追加されていることが分かります。

    ブランチ main
    追跡されていないファイル:
      (use "git add <file>..." to include in what will be committed)
        .gemini/commands/speckit.analyze.toml
        .gemini/commands/speckit.checklist.toml
        ...(中略)...
        .gemini/commands/speckit.tasks.toml
        .specify/

    .specify/ディレクトリにはmemory/, scripts/, templates/といったサブディレクトリが含まれます。

    🧹 古いファイルの削除手順

    新しいコマンドファイルと競合する古いファイルを特定し、手動で削除する必要がありました。

    1. 古いコマンド定義ファイルの削除

      cd .gemini/commands/
      rm plan.toml
      rm specify.toml
      rm tasks.toml
      cd -
    2. 古い.specify関連ファイルの削除

      rm -rf memory/
      rm -rf templates/
      rm -rf scripts/

    💡 ヒント: コマンド定義ファイルが管理対象外になるように、.gitignore.geminiを追加することも検討できます。ただし、その場合はgit statusには表示されなくなります。


    2. 既存プロジェクト(py-chessboardjs)でのspec-kit導入と運用

    既存のプロジェクトpy-chessboardjsakuroiwa/py-chessboardjs)でもspec-kitを導入しました。

    🛠️ 導入手順

    プロジェクトのルートディレクトリで、以下の手順を実行します。

    uv venv
    source .venv/bin/activate
    uv pip install -e . # プロジェクト依存パッケージのインストール
    uv tool install specify-cli --from git+https://github.com/github/spec-kit.git # spec-kitのインストール
    specify init --here # プロジェクト設定ファイルの作成
    specify --help # 動作確認

    spec-kitをgemini-cliで使用し始めると、プロジェクトルートに**GEMINI.md**というファイルが作成され、これがAIとの対話の出発点となります。

    🚀 spec-kitを利用した開発サイクル

    spec-kitのコアな使い方は、事前にGeminiにチャットで相談し、戦略を固めてからスラッシュコマンドを実行していく流れが最も効果的です。

    コマンド 目的
    /speckit.specify <文字列> 要件定義や仕様策定をAIに依頼し、**specs/**にMarkdownファイルを作成させる
    /speckit.plan 実装計画を立てる
    /speckit.tasks タスクリストを作成する
    /speckit.implement 実際にコードの実装を依頼する

    これらの作業を通して、specs/以下にMarkdownファイルが作成されます。原則として、specs/以下のサブディレクトリは作業ごとに作成されたブランチ名と同じになります。

    🔄 作業終了後のブランチマージ

    spec-kitがブランチを作成して作業を進めた後、作業をメインブランチに反映するには、以下の手順が必要です。

    git commit -m "修正内容のメッセージ" # 作業ブランチで必ずコミットする
    git checkout main
    git merge 001-fix-settings-load-error # 作業ブランチをmainにマージ
    git branch -d 001-fix-settings-load-error # 作業ブランチを削除

    3. GitHub ActionsとPyPI Trusted Publisherの設定

    パッケージの自動公開のために、GitHub Actionsのワークフローを.github/workflows/にYAMLファイルで記述します。このYAMLファイルはGeminiに依頼して作成してもらうことが可能ですが、動作確認が不可欠です。

    🚢 パッケージ公開ワークフロー

    py-chessboardjsのワークフロー例

    このYAMLファイルを使用して、PyPIのTrusted Publisherを登録します。登録時には、上記のYAMLファイル名を指定する必要があります。これにより、GitHub ActionsとPyPIが連携し、OIDC(OpenID Connect)を使って安全にパッケージを公開できます。

    🧪 テスト公開と本番公開

    タグ名によって、TestPyPIと本番PyPIへの公開を分けます。

    公開先 タグ名の条件 コマンド例
    TestPyPI -testを含むタグ名 git tag v0.0.3-test1
    本番PyPI -testを含まないタグ名 git tag v0.0.3

    タグを作成したら、git push origin <タグ名>でプッシュすることで、GitHub Actionsが起動します。

    📝 YAMLの構文チェック: Emacsではflycheck-yamllintをインストールすると、flycheck-modeでYAMLの構文エラーをリアルタイムに確認でき、大変便利です。


    4. AIとの協調作業における重要な教訓

    py-chessboardjsの開発中に遭遇した2つの不具合は、AIによる開発支援の限界と可能性を示す良い事例となりました。

    不具合1: ファイルパスの取得問題(C:\fakepath\

    現象: チェスエンジンのパス指定が機能しないエラーが発生し、出力にはFileNotFoundError: [Errno 2] そのようなファイルやディレクトリはありません: 'C:\\fakepath\\stockfish'とありました。

    解決: Google検索の結果、「fakepath」はHTMLのファイル入力フィールドがセキュリティ上の理由から実際のパスを隠蔽するブラウザの挙動であることが判明しました。この情報をGeminiに提供したところ、**「input type="file"をボタンに置き換え、pywebview.apiを介してPythonのネイティブファイルダイアログを呼び出す」**という正確な解決策が提示され、実装に成功しました。

    不具合2: キャスリング時のエラー(concurrent.futures.CancelledError

    現象: キャスリングを行うとエラーが発生しました。

    解決: ログからconcurrent.futures.CancelledErrorが見つかり、並列処理の不具合が疑われました。原因は、プロジェクトが使用しているchessboardjsの古いバージョンにあり、キャスリング時にFEN(盤面情報)が複数回更新され、チェスエンジンが処理しきれなかった可能性が高いと特定しました。

    以下はGeminiによる解説です:

    1. python-chessエンジン呼び出しの問題について

      根本的な問題は、GUIフレームワークであるpywebviewと、asyncioベースのエンジン通信を行うpython-chessライブラリとの間の並行処理の競合でした。

      • pywebviewのAPI呼び出しは同期かつ別スレッドで実行される: JavaScriptからpywebview.api.some_function()を呼び出すと、pywebviewは新しいPythonスレッドでsome_functionを実行し、その関数が戻るまでスレッドをブロックします。

      • python-chessのエンジン通信は非同期 (asyncio):
        chess.engine.popen_uci()関数やengine.play()メソッドは、Pythonのasyncioライブラリを使用して外部のチェスエンジンプロセス(Stockfishなど)と通信します。asyncioはイベントループ上でタスクを実行し、通常は単一のスレッドで行われます。

      • 競合 (レースコンディション):

        1. 以前のApi.uci_engine_move()は、await self.engine.play()を直接呼び出していました。このawait呼び出しは、エンジンが指し手を返すまでpywebviewのAPIスレッドをブロックしていました。
        2. このpywebviewのAPIスレッドがブロックされている間に、ユーザーが別のUI操作(例: 「リセット」ボタンや「戻る」ボタンのクリック)を行うと、その操作もpywebviewのAPI呼び出し(例: Api.on_closed())をトリガーしていました。
        3. 古い設計のApi.on_closed()メソッドはself.engine.quit()を呼び出し、エンジンプロセスを終了させていました。
        4. エンジンプロセスが終了すると、self.engine.play()が待機していたasyncioのFutureがキャンセルされ、concurrent.futures.CancelledErrorが発生しました。エンジンプロセスが予期せず終了した場合はchess.engine.EngineTerminatedErrorが発生しました。
        5. pywebviewのAPIスレッドはself.engine.play()の結果を待ってブロックされていたため、Futureがキャンセルされると、CancelledError(またはEngineTerminatedError)が伝播し、アプリケーションがクラッシュしたり、一貫性のない状態になったりしていました。
      • 解決策 (専用ワーカースレッド):

        • 私たちは、専用のthreading.Threadで実行されるEngineWorkerクラスを導入しました。
        • このEngineWorkerスレッドは独自のasyncioイベントループを持ちます。
        • pywebviewからApi.uci_engine_move()が呼び出されても、もはやブロックされません。代わりに、EngineWorkerが監視するqueue.Queueに「プレイ」タスクを投入します。
        • EngineWorkerはタスクを受け取り、自身の asyncioイベントループ上でawait self.engine.play()を実行します。指し手が準備できると、スレッドセーフなコールバック(_engine_move_callback)を使用して結果をApiクラスに返します。
        • Apiクラスはself.window.evaluate_js()を使用して、指し手をJavaScriptのフロントエンドにプッシュします。
        • なぜこれで解決したのか: pywebviewのAPIスレッドはasyncio操作によってブロックされなくなりました。asyncioイベントループは独自の安定したスレッドで継続的に実行されます。エンジンを終了させる可能性のあるUI操作は、EngineWorkerのキューに「終了」タスクを送信するようになり、ワーカースレッドはエンジンとそのループを安全にシャットダウンできます。計算中に「終了」が受信されても、ワーカースレッド内でasyncioタスクがキャンセルされるだけで、pywebviewのAPIスレッドには伝播せず、クラッシュを防ぎます。Engine move discardedというメッセージは、この正常な処理を示しています。
    2. specs/002-fix-castling-errorの削除について

      削除したのは良い判断でした。002-fix-castling-errorブランチとその関連する仕様ファイルは、バグ修正の失敗した試みを表していました。それらを残しておくと、誤った情報や置き換えられた情報でリポジトリが煩雑になります。

      フィーチャーの試みが失敗したり、新しいアプローチに置き換えられたりした場合は、古いブランチとその成果物をクリーンアップするのがベストプラクティスです。これにより、プロジェクトの履歴がクリーンに保たれ、成功した作業や関連する作業のみが反映されます。

      新しい003-fix-engine-concurrencyブランチとその仕様は、根本的な並行処理の問題を解決するための現在の成功したアプローチを正確に反映しています。

    💡 教訓: ユーザーの根気強さが成功の鍵

    これらの事例から学んだのは、Geminiでも不具合の特定は難しく、ユーザーとの共同作業であるということです。特に、エラー出力や挙動を注意深く観察し、根気強くAIにフィードバックすることが、適切な解決策を導き出すために不可欠となります。AIは強力な助けとなりますが、最終的なデバッグと検証は人間の役割です。

  • PywebviewでPythonとJavaScriptを連携:Promiseの役割と具体例

    Pywebviewは、PythonとJavaScriptを連携させたデスクトップアプリケーション開発に役立つフレームワークです。しかし、Pythonプログラマーにとって慣れない「非同期処理」や「Promise」に直面することもあります。本記事では、py-chessboardjsを例に、以下を詳しく解説します:

    1. Promiseとは何か、なぜ必要か
    2. Pythonのメソッド戻り値をJavaScriptの関数に活用する方法
    3. 具体例:PGNファイルの読み込み

    Promiseとは?

    Promiseは、非同期処理の結果を待つためのオブジェクトであり、成功(resolved)または失敗(rejected)の状態を持ちます。

    • Pythonでは、関数は通常即座に戻り値を返します(同期処理)。
    • 一方、JavaScriptでは処理が非同期になることが多く、処理完了後に値を受け取る必要があります。

    Pythonでは処理が終わるのを待って変数等に代入してくれますが、JavaScriptでは待ってくれません。
    これを解決するために、PywebviewはPythonのメソッド呼び出し結果をPromiseでラップします。これにより、JavaScript側でthenを使って結果を受け取れるようになります。


    Pythonメソッドの戻り値をJavaScriptで利用する

    以下は、py-chessboardjsのPGNファイル読み込み機能を使い、Pythonのメソッド結果をJavaScriptで処理する例です。

    Python側のコード

    PythonでPGNファイルを読み込み、その状態(FEN文字列)を返すメソッドを定義します:

    class Api:
        def open_pgn_dialog(self):
            """
            ユーザーにファイルダイアログを表示し、選択したPGNファイルを読み込み、
            そのゲーム状態をFEN文字列で返す。
            """
            file_types = ('PGN File (*.pgn)', 'All files (*.*)')
            file_path = self.window.create_file_dialog(webview.OPEN_DIALOG, file_types=file_types)[0]
            if os.path.exists(file_path):
                self.pgn = open(file_path)
                self.load_games_from_file()
                return self.board.fen()  # 現在のFEN文字列を返す
            return None

    JavaScript側のコード

    JavaScriptで、このPythonメソッドを呼び出し、結果(FEN文字列)を利用します:

    function openPgnDialog() {
        // Python API を呼び出し
        pywebview.api.open_pgn_dialog().then(fen => {
            if (fen) {
                console.log('FEN文字列:', fen);
                // 必要に応じてフロントエンドで処理
                updateBoardWithFen(fen);
            } else {
                console.error('PGNファイルの読み込みに失敗しました。');
            }
        }).catch(error => {
            console.error('エラーが発生しました:', error);
        });
    }
    
    // FEN文字列でボードを更新する例(仮)
    function updateBoardWithFen(fen) {
        alert('新しいFEN: ' + fen);
    }

    ポイント:
    pywebview.api.open_pgn_dialog()はPromiseを返し、thenで結果を受け取り処理しています。


    Promiseの基本的な使い方(Pythonプログラマー向け)

    PromiseはPythonの「関数の戻り値」をJavaScriptの非同期処理に適応するための仕組みです。

    • Pythonの戻り値はPromiseの成功状態(resolved value)として扱われます。
    • JavaScriptでは、thenを使って成功時の値を受け取ります。

    Promiseの流れ

    1. Pythonメソッドが値を返す。
    2. PywebviewがPromiseに変換。
    3. JavaScriptでthenを使い値を処理。

    フロントエンドのUIデザイン:Bootstrapを活用

    PywebviewはHTML/CSS/JavaScriptフレームワークを使用できるため、py-chessboardjsではBootstrapを活用し、ユーザーにとって使いやすいUIを構築しています。

    実装例

    以下は、index.htmlのナビゲーションバー部分です(省略のために一部改変):

    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="#">Py-Chessboardjs</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDarkDropdown" aria-controls="navbarNavDarkDropdown" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNavDarkDropdown">
                <ul class="navbar-nav">
                    <li class="nav-item"><a class="nav-link" href="#">Home</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Settings</a></li>
                </ul>
            </div>
        </div>
    </nav>

    結果、レスポンシブなナビゲーションバーが表示され、簡単にカスタマイズできます。


    まとめ

    Promiseは、PythonとJavaScriptの動作の違いを吸収するために必要な仕組みです。今回の例では、py-chessboardjsを使いながら以下を学びました:

    1. PythonメソッドをJavaScriptから呼び出し、戻り値をPromiseで扱う方法。
    2. Bootstrapを利用したスタイリッシュなフロントエンドデザイン。

    これらを応用すれば、直感的で高性能なアプリケーションを構築できます。


    アロー関数を使う理由

    今回、ChatGPTとClaudeに記事を書いてもらう際にpy-chessboardjsのコードを標準の書き方に修正してもらい、その理由を解説してもらいました。以下はその内容です。
    JavaScriptの=>アロー関数(arrow function)を意味し、関数を簡潔に書くための記法です。従来のfunctionキーワードを使った関数定義と同じ動作をしますが、以下のような利点があります。

    1. 簡潔な記法:

      • 従来のfunctionキーワードを省略できます。
      • 例えば、以下のようなコードが簡略化されます:

        // 通常の関数
        function add(a, b) {
         return a + b;
        }
        
        // アロー関数
        const add = (a, b) => a + b;
    2. thisの挙動が直感的:

      • アロー関数は親スコープのthisをそのまま使用します。一方、従来のfunctionは呼び出し方によってthisが変わるため、意図しない挙動を防げます。

        例:

        function Example() {
         this.value = 42;
        
         // 通常の関数(意図したthisではない可能性あり)
         setTimeout(function() {
             console.log(this.value); // undefined(thisはグローバルを指す)
         }, 1000);
        
         // アロー関数(意図したthisを保持)
         setTimeout(() => {
             console.log(this.value); // 42
         }, 1000);
        }
        new Example();
    3. Promiseチェーンでの一般的な書き方:

      • Promiseチェーン(thencatch)内で、アロー関数を使うのが一般的です。
      • シンプルなコールバック処理に適しています:

        // 通常の関数
        pywebview.api.get_data().then(function(result) {
         console.log(result);
        });
        
        // アロー関数
        pywebview.api.get_data().then(result => {
         console.log(result);
        });

    アロー関数が好まれるケース

    • 短い関数(関数リテラル): アロー関数は、数行で完結する関数リテラルに適しています。
    • コールバック関数: 非同期処理(setTimeoutPromiseなど)で使うと直感的です。

    アロー関数を避けるべきケース

    アロー関数はthisの挙動が予測しにくいケースがあります。

    • thisを明示的に保持する必要がある場合: メソッド内やイベントリスナーなど、thisの挙動が重要な場合は、従来の関数式を使用する方が安全です。
    • thisの再定義が必要な場合: アロー関数ではthisを新たに定義できません。そのため、例えばイベントリスナーの削除が必要な場合は避けるべきです。

      // イベントリスナーの登録
      button.addEventListener('click', () => console.log('clicked'));
      
      // イベントリスナーの削除(動作しない)
      button.removeEventListener('click', () => console.log('clicked')); // 別の関数として扱われる

    アロー関数は実際にはthisを明示的に保持するのに適していません。むしろ、アロー関数はthisを現在のスコープから継承するため、メソッドやイベントリスナーでは注意が必要です。

    アロー関数はthisの挙動が独特で、以下のような特徴があります:

    1. アロー関数は、定義された場所のthisコンテキストを継承します。
    2. 自身のthisを持たず、外側のスコープのthisをそのまま使用します。
    3. メソッドやイベントリスナーでは、通常の関数式の方がthisの参照が明確です。

    結論

    =>(アロー関数)を使った理由:
    本記事ではPromiseチェーンでの一般的な記法を重視し、可読性と簡潔さのためにアロー関数を使用しました。特に短いコールバック関数を記述する場合、アロー関数が現代的で簡潔な書き方として推奨されるケースが多いです。
    ただし、もし従来のfunctionキーワードに慣れている場合、どちらを使うかは個人のスタイルやプロジェクトのコーディング規約に依存します。