コンテンツにスキップ

アーキテクチャ概要

pyfltrの実装構造と主要な設計判断をまとめる。 本ページは保守・拡張に携わる開発者向け。 利用者向けの機能解説はCLIコマンド設定項目を参照する。

実行パイプライン

pyfltr.cli.pipeline.run_pipeline()がCLI/MCPの双方から呼び出される最上位エントリ。 TUI/非TUIの分岐はこの関数の内側で行い、パイプライン共通の前処理(ファイル展開・--only-failedフィルタリング・ アーカイブ初期化など)はTUI起動より前に集約する。

実行ステージは次の3段で構成する。

  1. fixステージ — {command}-fix-argsが定義された有効なlinterを順次--fix付きで実行する(ciサブコマンドは無効)
  2. formatterステージ — ruff-formatprettier等のformatterを直列または並列で実行する
  3. linter/testerステージ — 残りのlinter/testerを並列実行する

各ステージの結果はCommandResultに集約され、ステージ完了ごとにarchive_hookへ渡される。 ステージ間の中断(--fail-fast時の打ち切りなど)はstage_runnerの共通ヘルパーで吸収する。

モジュール構成

pyfltrのソースコードはpyfltr/直下に7つのサブパッケージと少数のトップレベルモジュールで構成する。 各サブパッケージは責務別に分離し、命名は責務に沿う。 ガイダンス系(precommit_guidance等)はcli配下、実行系はcommand配下に置く。

__init__.pyではre-exportせず、利用側はサブパッケージ内の具体モジュールから直接importする。 pyfltrはCLIツールであり、Pythonモジュールパスは内部実装として扱う。 内部リファクタリングではPython API互換性を維持しない。

サブパッケージ

  • pyfltr/cli/: CLIエントリポイントと各サブコマンドのハンドラー。 argparse構築・パイプライン本体・text整形描画・出力形式解決・ログ設定・MCP経路を担う
  • pyfltr/command/: コマンド実行コア。 実行コンテキスト型・プロセス管理・mise統合・subprocess環境構築・runner解決を担う。 対象ファイル選定・ファイル変更検知・ビルトインコマンド定義・エラーパーサー・ 2段階処理(ruff/prettier/taplo/shfmt)も含む。 コマンドのディスパッチはdispatcher本体に置き、ツール解決失敗ハンドリングはtool_resolution、サブプロジェクト単位の走査ループはsubproject_loopへ分離する。 subproject_loopは循環importを避けるため、ディスパッチ関数と無効スキップ結果生成関数をコールバックで受け取る方式を採用する
  • pyfltr/config/: 設定の読み書き・解決とプリセット定義を担う。 ビルトインツール定義の実体はcommand/builtin.pyが持つが、 config/config.py内のロジックでも参照するためfrom pyfltr.command.builtin import ...で取り込みつつ、 利用側コードの便宜のため__all__にも含めて再エクスポート扱いとする
  • pyfltr/output/: text・JSONL・SARIF・GitHub Annotations・GitLab Code Quality・Textual UIの 出力フォーマット群とツール別ルールURL生成を担う
  • pyfltr/grep_/: 横断検索・置換のコアロジック。 パターンコンパイル・ファイル走査・マッチ抽出・置換適用・replace履歴世代管理・ JSONLレコード生成・人間向け出力を担う
  • pyfltr/state/: アーカイブ・キャッシュ・履歴・再実行制御の永続化系。 実行アーカイブ読み書き・ファイルhashキャッシュ・list-runs/show-runサブコマンド・ --only-failedフィルター処理・retry_command生成・コマンド実行順制御・ステージ実行ヘルパーを担う
  • pyfltr/colloquial/: 口語表現チェッカー内蔵linter用のロジック・辞書・CLI一式。 denylist・allowlist正規表現の読み込みとマスク処理・python -m pyfltr.colloquialエントリを担う

トップレベルモジュール

  • pyfltr/paths.py: パスユーティリティ
  • pyfltr/warnings_.py: 警告蓄積

サブパッケージ間依存

サブパッケージ追加・モジュール移動の際の判断材料として、主要な依存方向を示す。 矢印は「import元 → import先」を示し、テスト・トップレベル汎用モジュール(pathswarnings_)への参照は省略する。

flowchart LR
    cli[cli/]
    command[command/]
    output[output/]
    state[state/]
    config[config/]
    cli --> command
    cli --> output
    cli --> state
    cli --> config
    command --> config
    command --> state
    output --> command
    output --> state
    state --> command
    state --> config

outputstateは実行結果であるCommandResultを扱うためにcommand/core_を参照する。 statecommand/targetsfilter_by_globs等も参照する。 逆方向(commandoutput / stateの参照)は循環を避けるため発生させない。 新規サブパッケージを追加する場合はこの方向ルールに従う。

サブコマンドとargparse

subparsersをrequired=Trueで必須化し、引数なし実行時のフォールバック挙動は持たない。 サブコマンド別の既定値は_apply_subcommand_defaults()で手動注入する。 set_defaults()を避けたのは、共通親パーサーを継承したサブパーサーに対して 他サブパーサーのdefaultが書き換わる既知挙動を回避するため。

サブコマンド一覧と用途はCLIコマンドを参照。

主要な設計判断

言語カテゴリはゲートとして働く

python / javascript / rust / dotnetの各言語カテゴリに属するツールは既定で無効(カテゴリキーが既定false)。 対象外プロジェクトで意図しないツール実行が起こることを避けるため、対応する言語カテゴリキー(例: python = true)で ゲートを開けるか、{command} = trueの個別明示が必要。

プリセットは各時点の推奨ツール構成を示すスナップショットで言語別ツールも含むが、 カテゴリキーがfalseのままだとプリセット由来の該当ツールtrueはゲート処理でfalseに上書きされる。 個別{command} = trueはゲートを越えて最優先される。 Python系ツール一式は本体依存に同梱されており、uvx pyfltr単発で利用できる。 JavaScript / Rust / .NET系は各言語のツールチェイン(Node.js・cargo・dotnet CLI)が前提のため、 pyfltr本体はこれらの依存を抱えない。

代替案として「完全別パッケージ(pyfltr-python)に分離」も検討したが、リポジトリ・リリース・バージョン整合の 複雑度が増し、利用者体験も劣るため不採用とした。

必須依存は最小化

本体必須依存は次の役割に限定する。

  • 骨組み: textual(TUI)・natsort(自然順ソート)・pyyaml(pre-commit設定)
  • run_id生成: python-ulid
  • MCP同梱: mcpplatformdirs
  • プロセス判定: psutilgit commit経由起動を親系列で検出してMM状態ガイダンスを出力する用途)

mcpを本体必須に含めるのはサーバー同梱体験(pyfltr mcpが即座に起動できる)を保つため。

subprocess実行はPopen一本化

subprocess起動はsubprocess.Popenベースに統一する。 --fail-fastの中断処理(外部スレッドからのterminate()呼び出し)が成立する基盤として必要。 パイプライン外で動くmise --versiongit check-ignorecls/clearはこの方針の対象外とする。

cli/pipeline.py/output/ui.pyの共通化はヘルパーに限定する

cli/pipeline.pyは直接呼び出し、output/ui.pyはRich UIへのcall_from_thread埋め込みという構造差がある。 完全共通化はlock取得タイミング差で実装が複雑になるため、共通化はstate/stage_runner.pyの小さなヘルパーへの抽出に留める。 残余重複は# pylint: disable=duplicate-codeを理由コメント付きで維持する。

ツール解決の失敗扱い

bin-runner / js-runnerによるツール起動解決は、対象ファイル0件のときは省略する。 mise等の解決はネットワーク制約・プラットフォーム制約で失敗し得るため、 解決不要な状況で副作用的な失敗を発生させないように早期returnする。

対象ファイルがあるにもかかわらず解決に失敗した場合は、resolution_failedという専用ステータスで返す。 通常の実行失敗(failed)と区別することで、CIログから 「対象0件で実行をスキップした」のか「対象はあったが解決時点で失敗した」のかを判別可能にする。 exit code判定・--only-failedの対象抽出・UI表示はいずれも両者を同等の失敗系として扱う。

CLI起動時のPATH整理とmise向けenv調整

CLI起動時にos.environ["PATH"]の重複エントリを順序先勝ちで除去し、 mise経由のsubprocessにはmiseが注入したtoolパスを除外したPATHを渡す。 親PATHにmise自身のtoolエントリが見つかると、miseがtools解決をスキップしてPATH解決へフォールバックするため、 これを避けるための対症療法である。 詳細な判定ロジックと比較キーはpyfltr/command/env.pybuild_subprocess_envを参照。

mise active tools取得結果の構造化

mise ls --current --jsonの取得結果はMiseActiveToolsResult構造体(pyfltr/command/mise.py)で扱う。 持つフィールドはstatus / tools / detailの3つ。 ステータスはok / mise-not-found / untrusted-no-side-effects等の7値を取る。 プロセス内キャッシュも本構造体のまま保持する。 これによりtool spec省略判定(_is_tool_active_in_mise_config)・JSONL header露出・command-info出力の 3経路で同じ結果を共有する。 取得時刻のずれによる不整合を排除する目的。 利用者が「tool spec省略未発動の理由」を診断できるようにする。

Python系ツールのpython-runner経由解決

Python系ツールの{command}-runner既定値は"python-runner"とする。 対象はruff-format / ruff-check / mypy / pylint / pyright / ty / uv-sort / pytestの8ツール。 "python-runner"はグローバルpython-runner設定(既定"uv"、許容値direct / uv / uvxの3値)へ委譲する。 uv経路ではcwdにuv.lockがありuvが利用可能な場合にプロジェクトのvenv経由で起動する。 いずれかが欠ける場合は本体依存に同梱されたバイナリへdirectフォールバックする。 uvx経路はuv.lockを参照せず{command}-version設定とも連動しない。

uv / uvx経路の追跡情報はJSONL headerと各commandレコードに出力する。 commandレコードのeffective_runner / runner_source / runner_fallbackは 「期待した経路と実際の経路が乖離した場合」のみ出力する(fallback検出用)。 通常経路では省略してLLM入力のトークン消費を抑え、通常時の解決状況の確認は pyfltr command-infoの責務とする。 利用者プロジェクトに当該ツールが未登録の状態でuv run --frozenが失敗した場合は、 uv add --dev "pyfltr[python]"を案内する警告を発行する。

モノレポ対応

起点cwd配下に複数の pyproject.toml を検出した場合、サブプロジェクト単位でツールを分割実行する。 検出ロジック・分類・uv workspace解釈・uv.lock 親方向探索は pyfltr/command/subprojects.py に集約する。

設計上の判断:

  • 検出は pyproject.toml の存在のみで判定する([tool.pyfltr] の有無は問わない)。 これにより既存モノレポを移行する際、各サブプロジェクトに設定追加を強いない
  • 検出結果が0件または1件の場合はモノレポモード非適用とし、従来通り単一実行する
  • ツール×サブプロジェクトの分割は CommandInfo.subproject_aware フラグで個別制御する。 プロジェクトローカル設定・モジュール解決・lockfileをcwd起点で読むツール (Python系・JS系・Rust系・.NET系のlinter/testerなど)は既定True。 リポジトリ単位で動作するツール(typosshellcheckshfmtpre-commit)は既定False
  • サブプロジェクト別の設定は当該ディレクトリで load_config(config_dir=cwd) を解決する。 起点と同一のCLIオーバーライドを再適用して ExecutionBaseContext.subproject_configs へ事前構築する。 ツールのON/OFF・除外・targets等をサブプロジェクト単位で尊重する
  • 実行対象コマンドは起点と各サブプロジェクトの有効集合の和で確定する。 subproject_aware=True のツールは起点またはいずれかのサブプロジェクトで有効なら対象に含め、 ループ内で各サブプロジェクトの設定によりON/OFFを再判定する(親OFF・子ON、親ON・子OFFの両方向に対応)。 和集合判定は config.is_command_enabled_anywhere に集約し、コマンド一覧の確定・ ステージ分割(split_commands_for_execution)・fix段抽出(filter_fix_commands)で共用する
  • subproject_aware の判定はツール特性を表すメタ設定のため起点設定で固定する。 リポジトリ単位ツール(subproject_aware=False)のON/OFFも起点設定で固定する。 外部パス(どのサブプロジェクトにも属さない起点cwd領域のファイル)への適用も起点設定のON/OFFで判定する
  • 親で有効でも全サブプロジェクトで無効化された場合、起点cwdで全ファイルをまとめて実行しない (「対象ファイル0件」と「設定による無効スキップ」を区別し、後者ではskipped結果で誤実行を抑止する)
  • ファイル所属判定は実体パスの「最深一致」で行い、ネストする子サブプロジェクト配下の ファイルは親側の集合に含めない
  • 出力スキーマは現行を維持する(JSONL・SARIF・archive・MCP読み取り系APIでサブプロジェクト 識別フィールドを新設しない)。サブプロジェクト境界をまたぐ実行結果は CommandResult.merge で 1件に集約し、人間向け output には # subproject: <相対パス> の区切り行のみ挿入する
  • subprocess.Popen へのcwd切り替えは引数渡しのみで実現する。os.chdir() を サブプロジェクトループで使うとプロセスcwdが並列実行中の他ツールへ干渉するため、 cwd依存処理(mise・git・ファイル走査・snapshot等)は明示引数で起点cwdを取得する形に統一する
  • uv workspace のmemberではcwd直下に uv.lock がない場合のみworkspace rootまで 親方向探索を許可する。無関係な祖先ディレクトリは越境しない

却下した代替案:

  • サブプロジェクトごとに別プロセスで pyfltr を起動する案。 mise再解決・起動コスト・出力統合の複雑化が、得られる独立性に見合わない
  • archive・JSONL・show-run・MCP読み取り系に subproject 識別フィールドを新設する案。 利用者指示「サブプロジェクト情報を全面表示しない」「現行の表示に近い」を満たさない
  • [tool.pyfltr] セクションを持つ pyproject.toml のみを検出する案。 既存モノレポを移行する際に各サブプロジェクトに設定追加を強いる

モジュール分割の方針

retry系ヘルパー・--only-failedフィルター・パス正規化・TUI/CLI共通ヘルパーは専用モジュールへ分離し、 各ファイルの肥大化を抑える。 run_pipeline()本体はcli/pipeline.pyに集約し、argparse構築はcli/parser.pyに、 エントリポイントとdispatchはcli/main.pyに置く。

具体的な分割先と内容はモジュール構成を参照。

実行アーカイブとファイルhashキャッシュ

pyfltrは2系統のユーザーキャッシュ基盤を持つ。 利用者向けの設定キーは設定項目を、OS別の既定パスは トラブルシューティングを参照。

保存ルートはplatformdirs.user_cache_dir("pyfltr", appauthor=False)で解決し、環境変数PYFLTR_CACHE_DIRで上書きできる。 プロジェクトローカルにキャッシュを生成しない方針を採用するのは、.gitignore運用の負担を増やさず、 複数プロジェクト横断での参照を可能にするため。

実行アーカイブ

エージェント連携時にJSONL出力のsmart truncationで除外された情報やツール生出力を事後参照可能にする。 list-runs/show-runサブコマンドおよびMCPの読み取り系ツール群は本アーカイブを単一の真実源とする。

run_idにはULIDを採用する。タイムスタンプ由来で辞書順ソート=時系列順ソートとなりlist-runsの実装が簡潔になる、 人が見たときに新旧の判別がしやすい、十分な衝突耐性を持つ、の3点が選定理由。

自動クリーンアップは世代数(archive-max-runs)・合計サイズ(archive-max-size-mb)・ 保存期間(archive-max-age-days)の3軸で制御する。 いずれかの閾値を超過した時点で古い順(run_id昇順)に削除する。 各設定値に0以下を指定すると当該軸の自動削除が無効化される。

書き込みはツール実行結果を受け取った直後の独立フックとして提供し、TUI経路・非TUI経路・ JSONL stdout有無のいずれでも発生する。 JSONL stdoutストリーミングとは独立した経路にすることで、どちらか一方を切り替えても他方が失われない。

既定で有効。--no-archiveまたはarchive = false設定で無効化できる。 オプトイン化(既定無効)は却下した。 エージェント連携時のUXを損なうため、既定有効+自動削除で肥大化を抑える設計とした。

アーカイブ用のシリアライズはLLM向け出力(llm_output.py)と独立した最小構造とし、 ErrorLocationの全フィールドを保存する。 rule_url等のフィールドが追加された際の追従コストを抑える狙い。

ファイルhashキャッシュ

同じ入力に対するツール再実行をスキップし、エージェント連携時の待ち時間と無駄な再計算を削減する。 対象は「ファイル間依存を持たず、設定ファイルもCWDでのみ解決するlinter」に限り、 CommandInfo.cacheable=Trueで明示する(現状はtextlintのみ)。

キャッシュキーには次の要素をsha256で連結する。

  • ツール固有: ツール名・実効コマンドライン・fix段かlint段か・構造化出力の設定値
  • 入力依存: 対象ファイル群のsha256・ツール固有設定ファイル群のsha256
  • 互換性: pyfltrのMAJORバージョン

誤ヒット防止が目的であり、ツール本体のバージョンは含めない(短期破棄前提で実害を許容)。

ヒット時はツール実行をスキップしてCommandResultを完全復元し、cached=True/cached_from=<ソースrun_id>を設定する。 アーカイブ書き込みは行わず(同じ結果を重複記録しない)、retry_commandも出力しない(再実行不要のため)。

<cache_root>/cache/<tool>/<hash>.json形式で保存する。 クリーンアップは期間軸(既定cache-max-age-hours=12)のみ。 サイズ・世代数の軸は採用しない(短期破棄前提でストレージ暴発リスクが小さいため)。

既定で有効。--no-cacheまたはcache = false設定で無効化できる。

カテゴリ別の対象外判定とその根拠はpyfltr/cache.pyモジュール冒頭docstringを参照。 formatter・tester・依存型linter・外部参照linter・階層型設定linterの5分類を扱う。 --config/--ignore-path検知時の安全側無効化も同所に記載する。

出力フォーマット

--output-formattext(既定)・jsonlsarifgithub-annotationscode-qualityの5種を持つ。 利用者向けのレコード書式はCLIコマンドを参照。 本節では設計判断を中心に扱う。

LLM向けガイダンス

JSONLはLLMエージェントが入力として読むケースが多いため、失敗時の次アクションと修正ヒントを明示的に同梱する。 summary.guidancecommand.hintsはいずれも英語で記す(トークン効率と汎用性のため)。 粒度・性質の異なる2種で使い分ける。

各フィールドの役割は粒度・性質で使い分ける。 フィールド単位の意味は自己説明性を優先する方針のため、別途のドキュメント記述は持たない。

  • command.hints: ruleごとの修正ヒント短文の辞書(ruleの識別子→1文ヒント)。 textlintコマンドの場合はmessages[].colキーでcol/end_colが累積位置である旨の仕様も同梱する。 空の場合はフィールドごと省略する
  • command.hint_urls: ruleごとのドキュメントURL辞書(ruleの識別子→URL)。 URLを生成できたruleのみ含み、空の場合はフィールドごと省略する
  • summary.applied_fixes: fixステージ・formatterステージで実際にファイル内容が変化した対象のパス一覧(ソート済み)。 変化がなかった場合は省略される
  • summary.guidance: failed + resolution_failed > 0のとき、またはapplied_fixesが非空のときに付与する英語の配列。 パイプライン全体の次アクションをbullet配列で示す。 失敗時はcommand.retry_commandの参照、--only-failed再実行、diagnostic.fixの解釈、 pyfltr show-run <run_id>の案内の4項目を並べる。 applied_fixes非空時はformatter/fix-stageの書き換えだけでは再実行が不要である旨の注記を末尾に追加する。 warningのみでfailed/resolution_failedが0件のケースでは付与しない(警告はパイプライン失敗を伴わないため)

command.hints / hint_urls 集約

修正ヒントとルールドキュメントURLはdiagnostic本体に含めず、commandレコードのhints / hint_urls辞書に集約する。 キーはrule IDで、形式は<plugin>/<rule>または単一rule名。 textlintコマンドの場合のみ、hintsmessages[].col参照記法のキーでcol/end_colの累積位置仕様を追加する。

集約は先勝ちで行う。複数のdiagnosticレコードで同一ruleに異なるhintが現れた場合は最初の値を採用し、 warningログを出力する。

hint_urlsはURLを生成できたruleのみ含み、空であればフィールドごと省略する。 hintsもヒントが1件も無ければフィールドごと省略する。 JSONL本体・tool.json・Pydanticモデルのいずれもキー名をhint_urls / hints(アンダースコア)で統一する。 JSON consumerがrecord["hint_urls"]等へドット記法アクセスできるようにするため、ハイフンは採用しない。

summary.commands_summary の0件省略

summary.commands_summary配下の統計フィールドは、状態判定に使う項目と付加情報で省略規則を使い分ける。

  • 常時出力(0件でも省略しない): succeeded / formatted / skipped / failed / warning。 特にfailed/warningは0件であること自体がエラーなし / 警告なし判定に直結するため、省略は不可
  • 0件で省略する: resolution_failed。ツール解決が成功する通常プロジェクトでは常に0件となる付加情報のため

warning{command}-severity = "warning"設定下で従来failed扱いだった結果が status="warning"に格下げされたものを集計する(パイプライン全体exit codeには影響しない)。 ツール起動自体に失敗したケース(resolution_failed / timeout_exceeded)はseverityの影響を受けない。

summaryレコードのフィールド順序

LLMが上から読み下したときに「結論→集計→指摘総数→ガイダンス→ファイル情報」の流れで把握できる順序に揃える。

  • 必須キー: kindexitcommands_summarydiagnostics
  • 条件付きキー: guidanceapplied_fixesfully_excluded_filesmissing_targets

コマンド単位の集計(statusカテゴリ別件数およびコマンド総数 total)は commands_summary 配下に集約し、 totalno_issues / needs_action の末尾へ置く。 指摘総件数 diagnostics はコマンド単位の集計ではなく commands_summary の外に並べる。

retry_command

当該ツール1件を再実行するshellコマンド文字列で、commandレコードに埋め込む。 構成要素は次の3点。

  • 起動プレフィックス: 親プロセスからuv run pyfltr/uvx pyfltr/pyfltrを判定する。 Linuxでは/proc/self/status経由、macOS/Windowsではargv basenameへフォールバックする
  • ベーステンプレート: 起動時のargvをコピーし、--commands値を当該ツールへ差し替え、位置引数を除去する
  • ターゲット: 当該ツールで失敗したファイルを絶対パス化して末尾に追加する。 --work-dir適用前の元cwdを基準とすることで、再実行時のcwd二重解釈を避ける

このためpyfltr ci失敗時のretry_commandpyfltr runが混入してfixステージが暴発することは無い。 キャッシュ復元結果(cached=True)ではretry_commandを埋めない。

smart truncationとアーカイブ復元

JSONL側で次の上限を適用する(pyproject.tomlで調整可能)。

  • jsonl-diagnostic-limit: 1ツールあたりの出力上限(集約前の個別指摘の合計で判定)。既定0(無制限)
  • jsonl-message-max-lines: command.message(生出力末尾)の行数上限。既定30
  • jsonl-message-max-chars: command.messageの文字数上限。既定2000

切り詰めの可否はアーカイブ書き込み成功フラグで判定する。 書き込み成功時のみ切り詰めを適用し、失敗時は全文をJSONLに出力する(復元不能な情報欠落の防止)。 fixステージと通常ステージを区別する必要があるため、判定単位はステージごとのCommandResult単位とする。

command.messageの切り詰めはハイブリッド方式で行う。 書式は「先頭ブロック + 中略マーカー \n... (truncated)\n + 末尾ブロック」。 冒頭にエラー要約を出力するツール(editorconfig-checker等)と、末尾にスタックトレースを出力するツール (pytest・mypy等)の双方を取りこぼさない狙いがある。

SARIF / GitHub Annotation / Code Quality

severityからの変換マップ。

  • SARIF level: error"error" / warning"warning" / info"note" / 未設定→"warning"
  • GitHub Annotation: error::error / warning::warning / info::notice / 未設定→::warning
  • Code Quality: error"major" / warning"minor" / info"info" / 未設定→"minor"

Code Qualityの仕様は5段階(info / minor / major / critical / blocker)だが、 pyfltr側に対応情報が無く過大評価を避けるため上位2段階は使わない。

GitHub Annotationのtitle{tool}: {rule}形式(ruleが無ければtool名のみ)。 本文は仕様に沿って%/改行をパーセントエンコードする。

Code Qualityのfingerprintはtool・file・line・col・rule・msgをタブ区切りで連結した文字列の SHA-256全桁を採用する。 同一指摘の重複統合に足るユニーク性を確保しつつ、配置順の変化に頑強にする。

logger 3系統と出力形式

pyfltrは3系統のloggerを使い分ける。

  • root(system logger): 常にstderr。抑止しない。設定エラー・アーカイブ初期化失敗などを送出する
  • pyfltr.textout: 人間向けテキスト出力(進捗・詳細・summary・warnings・--only-failed案内)
  • pyfltr.structured: 構造化出力(JSONL / SARIF / Code Quality)

pyfltr.textoutのformat別振る舞い(pyfltr.cli.output_format.configure_text_outputで設定)。

output_format output_file text stream text level
text(既定) 任意 stdout INFO
github-annotations 任意 stdout INFO
jsonl 未指定 stderr WARN
jsonl 指定 stdout INFO
sarif 未指定 stderr INFO
sarif 指定 stdout INFO
code-quality 未指定 stderr INFO
code-quality 指定 stdout INFO
任意 任意(MCP経路) stderr INFO

pyfltr.structuredのhandler設定(pyfltr.cli.output_format.configure_structured_outputで設定)。

  • jsonl / sarif / code-quality + --output-file未指定 → StreamHandler(sys.stdout)
  • jsonl / sarif / code-quality + --output-file指定 → FileHandler(output_file, mode="w", encoding="utf-8")
  • text / github-annotations → handler未設定(構造化出力は発生しない)

stdout占有が起きるのはjsonl / sarif / code-qualityかつ--output-file未指定時のみ。 MCP経路(pyfltr.cli.mcp_server.run_for_agent)は同一プロセス内でrun_pipelineを直接呼ぶ。 force_text_on_stderr=Trueを渡してtextloggerをstderrに強制する。 構造化出力は一時ファイル経由(FileHandler)となりstdoutを汚染しない。

詳細参照サブコマンドと再実行支援

実行アーカイブを参照するlist-runs/show-runサブコマンドと、--only-failed/--from-runによる 再実行支援の設計判断。 利用者向けの使い方はCLIコマンドを参照。

list-runs/show-runの実装配置

サブコマンド本体はpyfltr/state/runs.pyに集約する。 cli/main.pyconfig/generate-shell-completionと同じ「非実行系サブパーサー」として サブパーサー登録とディスパッチのみを行い、出力ロジックは持たない。

読み取り経路はArchiveStoreの既存APIを直接利用し、load_config()は呼ばない。 キャッシュルートの上書きは環境変数PYFLTR_CACHE_DIRのみで完結させて依存を最小化する。

「指定runの実保存ツール一覧」はtools/ディレクトリ走査をSSOTとする。 meta["commands"]は実行予定のリストで、--fail-fast中断やskippedで実際には保存されなかったツールを 含みうるため。

アーカイブの保存キーはツール名固定のため、同一ツール名のfixステージと通常ステージは 通常ステージで上書きされる。 show-runは各ツールの最終保存結果のみを参照可能で、ステージ別保存への拡張は対象外とする。

run_id解決は完全一致に加えて前方一致とlatestエイリアスを許容する。 解決ロジックはpyfltr/runs.pyresolve_run_id()に集約し、 MCPサーバー・--only-failedからも再利用する。

--only-failed

直前runから失敗ツールと失敗ファイルを抽出し、ツール別に失敗ファイル集合のみを対象として再実行する。

  • 直前runはArchiveStore.list_runs(limit=1)の先頭を採用する
  • 失敗ツール・失敗ファイルはアーカイブのtoolメタとdiagnosticsから抽出する
  • フィルタリング結果はツール別のToolTargets dataclass(pyfltr/only_failed.py)として保持する
  • 直前runが存在しない、失敗ツールが無い、ターゲット交差が空となった場合はメッセージを出力して 成功終了(rc=0)する
  • 位置引数targetsとの併用時は、直前runの失敗ファイル集合とtargetsを交差させる

フィルタリングはrun_pipeline内のファイル展開直後・archive/cache初期化前に行う。 今回のrunのrun_id/cache_storeに影響させないため。

--from-run

--only-failedの参照対象runをアーカイブの前方一致・latestエイリアスで明示指定する。

  • --from-run <RUN_ID>--only-failedとの併用のみを受け付け、単独指定はargparseエラーで拒否する
  • <RUN_ID>の解決はpyfltr/runs.pyresolve_run_id()を再利用する
  • 指定<RUN_ID>が存在しない場合は警告を出力してrc=0で早期終了する
  • 値および--only-failedフラグはretry_commandへ伝播させない

--from-run値はretry_commandへ伝播させない方針を採用する。 生成するretry_commandは「当該ツール+失敗ファイル」に固定されているため、 アーカイブ参照フラグを引き継ぐと再実行時に古いrunを暗黙参照し続けるリスクがある。

--from-run--only-failedなしで単独利用可能にする案も却下した。 --from-run単独ではdiagnostic参照は行われず意味を持たない。

MCPサーバー

pyfltr mcpサブコマンドが提供するMCP(Model Context Protocol)サーバーの設計判断。 利用者向けの起動方法・MCPツール一覧・MCPクライアント設定例はCLIコマンドを参照。

提供ツール構成

読み取り系4ツール(list_runsshow_runshow_run_diagnosticsshow_run_output)と 実行系1ツール(run_for_agent)の計5ツールを公開する。 実行系を1本に限定したのは、エージェント連携用途ではci/run/fastの差分を露出する必要が薄く、 パラメーター数を抑えてMCPスキーマを単純化するため。

ツール名はCLIサブコマンドのハイフン形式と異なりアンダースコア形式(list_runs/show_run等)とする。 ハイフンはPythonの@mcp.tool()名として非推奨のため。

MCPライブラリ

mcp.server.fastmcp.FastMCPを採用する。 高レベルDSLで記述量が最小、型ヒントからinputSchemaとoutputSchemaを自動生成可能、 stdioトランスポート起動がmcp.run(transport="stdio")の一行で済む点が決め手となった。 低レベルAPI(mcp.server.Server)の利点が必要となる動的capability交渉は不要。

stdio隔離

stdioトランスポートはstdin/stdoutをJSON-RPCフレームに専有するため、 どの経路であれstdoutへの書き込みはプロトコル破壊を引き起こす。 3層で隔離を実施する。

  1. 起動直後にroot loggerの出力先をstderrへ強制する
  2. run_for_agentツール内ではrun_pipelineforce_text_on_stderr=Trueを渡し、 人間向けtext整形loggerをstderrへ向ける。構造化出力は一時ファイルへFileHandler経由で出力する
  3. TUI起動経路(subprocess.run("clear")やTextual UI)はargs構築時に遮断する

logger初期化は全format共通経路に集約されているため、force_text_on_stderrの1フラグだけで MCP経路のstdin/stdout専有を守れる。

run_for_agentの実装経路

内部でargparse.Namespaceを構築し、run_pipelineを直接呼び出す。 run(sys_args=[...])経由でargparseに渡す案ではエラーメッセージのstderr出力制御が困難で、 MCPツール側でのエラー整形ができないため不採用。 外部プロセス起動(subprocess.run(["pyfltr", "run-for-agent", ...]))案も検討した。 プロセス管理・PYFLTR_CACHE_DIR伝搬・TERMシグナル・テスト安定性の面で同一プロセス方式より不利のため不採用。

run_pipeline()戻り値

run_pipeline()の戻り値は(exit_code, run_id_or_None)の2要素タプルとする。 2要素目はアーカイブ無効時・early exit時にNone、それ以外では採番済みULIDが入る。

only_failed有効時に「直前runなし」「失敗ツールなし」「対象ファイル交差が空」のいずれかに該当した場合、 run_pipelineはearly exit((0, None))を返す。 このときrun_for_agentはエラーではなく「実行スキップ」(skipped_reasonに理由文字列)を返す。

戻り値変更を採用したのは並行プロセス対策。 MCPツール側でArchiveStore.list_runs(limit=1)を引く案では、同一ユーザーキャッシュを参照する 並行プロセスがあると別runのrun_idを誤って拾うリスクがあるため戻り値経由とした。