コンテンツにスキップ

アーキテクチャ概要

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

実行パイプライン

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

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

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

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

fixステージの結果は同じツールの通常ステージと重複するため、成功時は最終結果へ含めず保存だけを行う。 fixステージが失敗した場合は、同じツールの通常ステージの結果(成功・スキップを含む)へ失敗と出力を統合した1件を 最終結果とし、表示・終了コード・retry_command・アーカイブへ同じ結果を渡す。 --fail-fastで停止する場合もfixステージの失敗を記録し、同じツールのスキップ結果を重ねない。 通常ステージの成功でfixステージの失敗が消えると、修正処理の失敗が利用者へ届かないためである。

層順とサブパッケージの責務

依存は次の下位から上位への7層に整理する。同じ層または下位だけをimportし、 関数内とTYPE_CHECKING内のimportにも同じ規則を適用する。 サブパッケージは9つで、command/two_step/はコマンド実行の内部構成である。

層 配置 責務
汎用 text/・colloquial/・parsing/とトップレベルモジュール フェンス走査・口語表現の検出・診断解析、tools.pyのツール定義、diagnostics.pyの型、rule_urls.pyのURL生成、パス・警告・実行オプション
設定 config/ 読込・統合・検証・編集・選択・プリセット。組込みツールの値はtools.pyから導出する
コマンド実行 command/ 実行要求・振り分け・プロセス管理・runner解決・対象選定、executor.pyとstage_runner.pyの実行制御、キャッシュ判定
出力 output/ text・JSONL・SARIF・GitHub Annotations・GitLab Code Quality・TUI、診断表示・logger設定
保存 state/ アーカイブとキャッシュの永続化・読取、run_id解決と再実行情報
検索と置換 grep_/ 対象解決・走査・置換・履歴・適応的出力
CLIとMCP cli/ 公開入力の解析、パイプライン、サブコマンド表示、出力形式選択、MCP接続

commandはキャッシュストア等をProtocolやコールバックで受け取り、stateとoutputへ依存しない。 outputとstateは実行結果のcommand/core_.pyを参照する。 command/only_failed.pyのToolTargetsは保存診断から渡された対象を解決し、 保存メタやdiagnosticsの読取はstate/only_failed.pyが担う。

__init__.pyではre-exportせず、具体モジュールから直接importする。 PythonモジュールパスはCLIツールの内部実装として扱い、内部移動の利用側は同時に追随する。 tests/integration/layers_test.pyが全PythonファイルのASTから逆方向のimportを検出する。

flowchart LR
    cli[CLI・MCP] --> grep[検索・置換]
    grep --> state[保存]
    state --> output[出力]
    output --> command[コマンド実行]
    command --> config[設定]
    config --> common[汎用]

図は層の許容順序を示す。隣接層を経由する必要はなく、任意の下位を参照できる。

必須依存は最小化

本体へ同梱する各チェックツールを除き、基盤の必須依存は次の役割に限定する。

  • 骨組み: textual(TUI)・natsort(自然順ソート)・pyyaml(pre-commit・prek設定)
  • run_id生成: python-ulid
  • MCP同梱: mcp・platformdirs
  • プロセス判定: psutil(git commit経由起動を親系列で検出し、formatterの修正がワークツリーには書き込まれる一方でindexには反映されない状態(MM状態)の案内を出力する用途)

mcpを本体必須に含めるのはサーバー同梱体験(pyfltr mcpが即座に起動できる)を保つため。 pre-commitとprekは同じ設定形式を使う代替実行系である。 既存環境との互換性を保ちつつ高速な実行系を選択できるよう、双方を本体へ同梱する。

モジュール分割の方針

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

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

設定の設計判断

言語カテゴリによる有効化の限定

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)に分離」も検討したが、リポジトリ・リリース・バージョン整合の 複雑度が増し、利用者体験も劣るため不採用とした。

コマンド実行の設計判断

subprocess実行はPopen一本化

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

CLIとTUIのステージ実行を共有する

command/stage_runner.pyがfix、formatter直列、linter/tester並列の実行順を所有する。 cli/pipeline.pyは直接実行し、output/ui.pyは画面通知・中断・結果差し替えをコールバックで渡す。 Rich UIへのcall_from_threadとlock取得の差は画面側に残し、ステージ処理の複製とarid抑止を除く。

ツール解決の失敗扱い

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

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

実行コンテキストに応じた子プロセスenvの正規化

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

実効runnerがuvのsubprocessには、継承したVIRTUAL_ENVを除いたenvを渡す。 uvは実効cwdからプロジェクト環境を解決するため、実効cwdと結び付かない親のVIRTUAL_ENVが残ると、 正常終了した実行にも環境不一致警告が出る。 除去だけを行い環境パスを算出しないのは、uvの解決規則をpyfltr側へ重複実装しないためである。

実行コンテキスト(実効cwdと実効runner)に結び付く親由来の環境変数は、起動箇所ごとに加工せず build_subprocess_envへ集約して正規化する。起動箇所で個別に加工すると、 子プロセスを起動する箇所を追加するたびに同じ判断が必要になり、正規化しないまま起動する箇所が残る。 詳細な判定ロジックと比較キーはpyfltr/command/env.pyのbuild_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"とする。 対象はpython = trueで有効化されるruff-format / ruff-check / mypy / pylint / pyright / ty / arid / uv-sort / pytestの9ツールに、 既定無効のbanditを加えた10ツールとする。 "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]"を案内する警告を発行する。

uvx既定のPython製ツール

DEFAULT_CONFIGで{command}-runner = "uvx"を既定とするPython製ツールは、 python-runnerへ委譲せずuvx <bin>で別環境へ解決する。 対象はsemgrepとsqlfluffで、pyfltr本体の依存グラフには含めない。 解決失敗時の分類はDEFAULT_CONFIGから導出し、runner既定値と独立したツール一覧を持たない。

本分類の対象は、既定値が無効でありpython = trueでも有効化されないツールである。 さらに、他の依存へ厳密ピンまたは上限制約を課すことを採用条件とする。 利用者が版を固定する場合は対象パッケージを利用者プロジェクトへ追加してdirect指定へ切り替えるか、 {command}-pathで実行ファイルを明示する。

モノレポ対応

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

設計上の判断:

  • 検出はマーカー(固定ファイル名pyproject.toml・Cargo.toml、globパターン*.csproj・ *.sln)の存在で判定する([tool.pyfltr]の有無は問わない)。これにより既存モノレポを 移行する際、各サブプロジェクトに設定追加を強いない。マーカー一覧は _SUBPROJECT_MARKER_NAMES・_SUBPROJECT_MARKER_GLOBS(pyfltr/command/subprojects.py)を 典拠とする。package.jsonはプロジェクト境界を持たない汎用ファイルとして 誤検出リスクが高いため対象外とする
  • 検出結果が0件または1件の場合はモノレポモード非適用とし、従来通り単一実行する
  • ツール×サブプロジェクトの分割は CommandInfo.subproject_aware フラグで個別制御する。 プロジェクトローカル設定・モジュール解決・lockfileをcwd起点で読むツール (Python系・JS系・Rust系・.NET系のlinter/testerなど)は既定True。 リポジトリ単位で動作するツール(typos・shellcheck・shfmt・pre-commit・prek)は既定False
  • サブプロジェクト別の設定は対象のディレクトリで load_config(config_dir=cwd) を解決する。 pyproject.tomlを持たないサブプロジェクト(Cargo.toml単独・*.csproj単独等)は [tool.pyfltr]の記述先が存在しない。 この場合、最も近い祖先のpyproject.tomlから継承元configを決定する。 探索対象は検出済みサブプロジェクトの真の祖先で深度が最も深いものとする。 該当祖先が無ければ起点configを継承する。 起点と同一のCLIオーバーライドを再適用して ExecutionBaseContext.subproject_configs へ事前構築する。 ツールのON/OFF・除外・targets等をサブプロジェクト単位で尊重する (継承時は継承元の値がそのまま適用される)
  • Cargo workspace root配下の登録member crateは重複実行防止のため候補集合から除外する。 workspace rootは[workspace]テーブルを持つCargo.tomlで判定する。 除外対象はmembersglob展開結果からexcludeglob展開結果を差し引いたディレクトリとする。 無登録crateは独立サブプロジェクトとして残す。 .sln(.NET solution)所在ディレクトリでは、.slnファイル内で登録されたproject所在ディレクトリのみ除外する。 無登録csprojは独立サブプロジェクトとして残す。 登録member・登録projectであっても、他のマーカー(pyproject.toml・ 登録csproj所在ディレクトリのCargo.toml)を併有するディレクトリは、 対象のディレクトリ固有の設定を尊重するため除外せず独立サブプロジェクトとして残す (この場合はworkspace・solutionルート側の一括実行と対象のディレクトリの個別実行が重複し得る。 重複を避けたい場合は対象のディレクトリへ他のマーカーを配置しない運用とする)
  • 実行対象コマンドは起点と各サブプロジェクトの有効集合の和で確定する。 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: <相対パス> の区切り行のみ挿入する
  • 診断のfileは解析境界(pyfltr.parsing.entry.parse_errors)で起点相対へ揃え、統合時には付け替えない。 通常・fix・2段階などの各実行処理はExecutionRequest.parse_errorsを通じてツールの実行cwd(path_base)と実行起点(start_cwd)を渡し、 pyfltr.paths.to_start_relativeが相対パスを実行cwd基準で絶対化してから起点相対へ変換する。 起点外を指すパスは../相対を含めて/区切りの絶対パスで返し、対象集合が保持する外部パスと同じ表現に揃える。 各パーサーはツール出力のパスを区切りの統一だけで返し、基準変換を持たない。 診断メッセージにもパスを埋め込むaridはCommandInfo.path_resolving_parserとして同じ変換関数を受け取る。 一時ファイルから元ファイルへの付け替え(file_path_remap)は変換の後に適用し、付け替え後の値は再変換しない。 --only-failedとretry_commandは起点相対の対象集合と診断のfileを文字列で突合するため、 基準が子プロジェクト相対のまま残ると失敗ファイルを選べない
  • 診断を起点相対へ揃える前の版が保存したpytestのアーカイブは、子プロジェクト相対の診断を持つ。 ArchiveStore.read_tool_diagnosticsは保存済み生出力の# subproject:区間ごとに解析をやり直し、 file・行・メッセージが一致する区間が1件だけの診断を起点相対へ補正して返す。 区間を一意に決められない診断は変えず、保存ファイルも書き換えない。 新しい版はrunのmeta.jsonへdiagnostic_paths: start-relativeを書き、このキーを持たないrunだけを補正の対象にする。 新しい版のrunを補正すると、起点直下の診断を同じ位置・メッセージを持つ子の診断と取り違えるためである。 診断のbasenameや末尾一致で所属を推測する案は、子プロジェクト間で同じ相対名を持つファイルを誤選択するため採らない
  • subprocess.Popen へのcwd切り替えは引数渡しのみで実現する。os.chdir() を サブプロジェクトループで使うとプロセスcwdが並列実行中の他ツールへ干渉するため、 cwd依存処理(mise・git・ファイル走査・snapshot等)は明示引数で起点cwdを取得する形に統一する
  • uv workspace のmemberではcwd直下に uv.lock がない場合のみworkspace rootまで 親方向探索を許可する。無関係な祖先ディレクトリは越境しない
  • サブプロジェクトループ全体を警告重複の抑止スコープとする。 スコープ内ではemit_warningの同一source・message組と、 add_filtered_direct_fileの同一path・reason組を1件だけ保持する。 独立project間で同一文言を発行する既存仕様と、 不在ターゲット件数に基づく終了判定を維持するため、スコープ外は重複を許容する

サブプロジェクト単位で繰り返す処理へ警告を追加する場合は、次の手順で件数を計測する。

  1. 同じツールを有効化したサブプロジェクトを2件以上用意する
  2. 起点と複数のサブプロジェクトで、同一のsourceとmessageを持つ警告を意図的に発生させる
  3. collected_warnings()から対象組を抽出し、件数が1件であることを確認する
  4. 外部パス除外を伴う場合は、filtered_direct_files(reason="external")の同一パスも1件であることを確認する

却下した代替案:

  • サブプロジェクトごとに別プロセスで pyfltr を起動する案。 mise再解決・起動コスト・出力統合の複雑化が、得られる独立性に見合わない
  • archive・JSONL・show-run・MCP読み取り系に subproject 識別フィールドを新設する案。 出力スキーマへサブプロジェクトの識別情報を含めず、現行の表示に近い形を保つという要件を満たさない
  • [tool.pyfltr] セクションを持つ pyproject.toml のみを検出する案。 既存モノレポを移行する際に各サブプロジェクトに設定追加を強いる
  • 1個の設定ロード結果から警告組の静的な積集合を作成し、 後続サブプロジェクトの抑止対象へ固定する案。 起点projectがglobalの不正値を正常値で上書きした場合は起点の結果に警告組が現れず、 不正値を上書きしない複数サブプロジェクト間の重複を防げない

pytest用の実行基盤(fast置換と常時追加)

pytest-fast-targetsを持つ設定がある場合、fast実行ではpytestだけが位置引数・--changed-sinceに依らない母集合を使う。 fast実行はfastサブコマンドと、--commandsにfastトークンを含む実行を指す。 pytest-always-targetsを持つ設定がある場合は、fastに限らずpytestの母集合へプロジェクト全域から同設定のglobに一致したファイルを加える。 run_pipelineはプロジェクト全域を走査した集合からpytest用のExecutionBaseContextを別に作成し、 ExecutionBaseContext.pytest_baseへ保持する。 dispatcher.execute_commandはpytestの実行時だけExecutionContext.baseをこの基盤へ差し替える。 _prepare_execution_paramsはpytest_fast_targets_activeが真の基盤で、対象globをpytest-fast-targetsへ置き換える。 pytest_always_targets_activeが真の基盤では、対象globの結果へpytest-always-targetsに一致するファイルを和集合として加える。 ExecutionContext.all_filesがサブプロジェクト分だけを返す契約は差し替え後の基盤でも保たれる。 指定を持たない設定(起点か各サブプロジェクト)の集合は差し替え前と同じものを保持する。 --only-failedは失敗ファイルと対象の交差で再実行対象を決めるため、pytest-always-targetsの一致ファイルも交差の候補へ加える。

却下した代替案:

  • dispatcherの対象globだけを差し替える案。母集合が変更ファイルのままのため、Markdownだけの変更や 変更ファイルの無いサブプロジェクトで対象テストを取りこぼす
  • 全コマンドへ全域の集合を渡す案。formatterとlinterの範囲まで広がる

ExecutionParams.targetsとCommandResult.target_filesの下流の流れ

両フィールドは実際に処理対象となったファイル集合を表す。 値の意味を変える差し替えを禁じ、意味論的な整合性を保つ必要がある。 下流の流れは次のとおり。

  • ExecutionParams.targets
    • キャッシュキー計算(state/cache.pyのcompute_key)
    • fix時のargv組み立て(command/textlint_fix.py)
  • CommandResult.target_files
    • キャッシュエントリのシリアライズ・復元(state/cache.py)
    • retry_command再構成と失敗集合との交差(state/retry.pyのbuild_retry_command・filter_failed_files)
    • dispatcher側の設定処理(command/dispatcher.pyの_run_plain_commandと_with_targetsが ExecutionParams.targetsから値を設定する)
    • サブプロジェクト結果統合時の和集合再構成(command/core_.pyのCommandResult.merge)

マスク済み一時パスなど値の意味を変える差し替えが必要な場合の代替手段は次の2つ。

  • ExecutionParams.cache_commandline: キャッシュキー計算用の元パス基準commandline
  • ExecutionParams.file_path_remap: 一時パスから元パスへの変換辞書

両フィールドは元パス基準でキャッシュキーとJSONL診断出力を安定化させるために設ける。

専用実行方式の追加

通常のツール追加はtools.pyのBUILTIN_COMMANDSへCommandInfoを1件登録し、 既存のexecution_kindと解析方式を選ぶ。実行方式そのものを追加する場合は、次の接続を更新する。

  1. tools.pyのExecutionKindへ種別を追加する。
  2. command/の担当モジュールにExecutionRequestを受け取りCommandResultを返す実行関数を実装する。 要求にある設定・対象・実行文脈を使い、対象展開と共通の完了判定を再定義しない。
  3. command/dispatcher.pyの_EXECUTORSへ種別と実行関数を登録する。
  4. 対象CommandInfo.execution_kindへ種別を指定する。fix段階の方式を追加した場合は fix_execution_kindへ指定する。
  5. 担当実行モジュールに対応するtests/command/で要求の受渡しと結果を検証する。

dispatcherのexecute_commandはこの登録表を使い、通常実行とfix段階を同じ要求型で接続する。

完了判定

対象到達と実行完了の判定は、pyfltr/command/completion.pyのevaluate_completionが1回の実行につき1度だけ導出する。 summaryのcompletion・files_reached・completed_commands・incomplete_commandsはこの値を返す。 MCP runの同名4項目とmissing_targets・fully_excluded_filesも同じ値を返す。 run_pipelineがearly exitを含む全ての終了箇所で導出し、JSONLへは出力文脈を通して、MCPへは戻り値として渡す。 出力側では判定条件を書かず、同じ入力から2つの出力が別々の結論を返す状態を構造的に防ぐ。 区分の導出条件は同関数のdocstringに置く。

対象ファイルが無いためのskip(CommandResult.not_applicable)を未完了に数えるのは、--commandsでコマンド名を明示した場合だけとする。 未指定時の全コマンド実行では対象の言語を持たないツールのskipが常に生じ、未完了に数えると区分が常にincompleteとなって判断に使えないためである。 --fail-fast・中断によるskipと、対象があるのにツールを起動できなかったskipは明示の有無によらず未完了とする。

数値終了コードと完了区分は役割を分ける。 終了コードは診断・formatterによる書き換え・ツール失敗を表す既存の契約である。 完了区分は指定したチェックが対象へ到達して評価を終えたかを表す。 診断を検出して終了コードが1になった実行も完了区分はcompletedとなり、 一部の対象が不在のまま他の対象のチェックが成功した実行は終了コード0でもincompleteとなる。

判定の入力は実行アーカイブではなく、パイプラインが保持する最終CommandResult列とする。 キャッシュから復元した結果は実行アーカイブへ記録されないため、MCPのcommandsのように アーカイブから組み立てると、キャッシュヒットしたコマンドが完了一覧から欠ける。

コーディングエージェントが主に使う呼び出し手段はMCP runとする。 CLI JSONLではheader・command・summary・warningの複数レコードと不在・全除外の情報を組み合わせないと 完了を判断できず、エージェントが同じ再集計をシェルで繰り返していたためである。 CLI JSONLはシェルしか使えない消費側向けの代替手段として残す。 同じ判定値をsummaryへ加えるため、CLI専用の判定分岐を持たずに同じ結論を返す。

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_commandにpyfltr runが混入してfixステージが暴発することは無い。 キャッシュ復元結果(cached=True)ではretry_commandを埋めない。 生成対象はCommandResult.needs_rerunが真の結果(statusがfailed・warning・resolution_failedのいずれか)に限る。

--only-failed

直前runから失敗ツールを抽出し、ツール別に対象ファイルを限定して再実行する。 対象の決め方は直前runの診断がファイルを持つかで分かれ、 pyfltr/state/only_failed.pyの_extract_failed_files_for_tool()が判定する。 command/only_failed.pyのToolTargets.resolve_files()が対象を返す。

  • 診断にfileを持つ失敗ツールはmode="files"とし、失敗ファイル集合と現在の対象ファイル一覧の交差を対象とする。 交差が空のツールは対象から除く
  • 診断にfileを持たない失敗ツール(pytestなどpass-filenames=False系)はmode="fallback"とし、 現在の対象ファイル一覧(all_files)をそのまま対象とする
  • 現在の対象ファイル一覧は位置引数targetsと--changed-sinceを適用した後の一覧である
  • 直前runはArchiveStore.list_runs(limit=1)の先頭を採用する
  • 失敗ツール・失敗ファイルはアーカイブのtoolメタとdiagnosticsから抽出する
  • 直前runが存在しない、失敗ツールが無い、ターゲット交差が空となった場合は、理由と対処を source="only-failed"の警告として発行して成功終了(rc=0)する。JSONL出力でもheader・warning・summaryを出力する。 text_loggerのINFOだけではJSONL出力(textはWARN以上)とMCPへ理由が届かないため、警告として発行する
  • モノレポ分割実行では、command/only_failed.pyのToolTargets.resolve_files()が対象のサブプロジェクトの対象ファイル一覧 (ExecutionContext.all_files)を受け取る。filesモードではこの一覧と交差させ、 起点cwd全体で抽出した失敗ファイル集合を所属しないサブプロジェクトへ渡さない。 fallbackモードではサブプロジェクト単位の一覧をそのまま返す

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

出力フォーマット

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

公開するファイル位置の表現

利用者へ公開するファイル位置の値は、生成方法によらず区切りを/へ統一する。 対象はJSON Lines出力・CLIテキスト出力・CLIのJSON出力・MCPツール応答・SARIF・Code Quality・ GitHub Annotationsの各出力で返すファイルパスである。 除外対象や欠落対象として警告へ載せるパスも含む。 生成時はpyfltr.paths.normalize_separators・pyfltr.paths.to_cwd_relative・pyfltr.paths.to_start_relativeのいずれかを経由する。 公開値をstr()で直接文字列化しない。 pyfltr自身が実装するツール(colloquial-checkなど)の標準出力に含めるファイル位置も、 生成時点で同じ正規化を経由する。これらのツールの生標準出力はshow-run --commands <name> --outputと MCPツール応答へ解析を経ずに載るため、pyfltr.parsing.entryの解析による 正規化だけでは契約を満たさない。 利用者や対話するエージェントが値を別の入力へ再利用するため、出力ごとの表現差は突合を失敗させる。

環境情報として提示する絶対パスはこの契約の対象外とし、OSネイティブ表現のまま返す。 該当する値はJSON Linesのheader.cwd、実行アーカイブのメタ情報のcwd、 configサブコマンドが返す設定ファイルパス、command-infoが返す実行ファイルパスである。 これらの値は利用者が実行環境を把握するために読み、出力をまたいだ突合には用いない。

コマンドライン引数、キャッシュキー、並び順キー、ファイルパスのremap辞書のキーなど、 内部処理で用いるパス文字列もこの契約の対象外とする。

LLM向けガイダンス

JSONLはLLMエージェントが入力として読むケースが多いため、失敗時の次アクションと修正ヒントを明示的に同梱する。 summary.guidance・command.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項目を並べる。 resolution_failedが1件以上ある場合は、再実行だけでは解消しないため、tool-resolve警告に従って ツールを導入するか設定を変える旨を先頭に加える。 applied_fixes非空時はformatter/fix-stageの書き換えだけでは再実行が不要である旨の注記を末尾に追加する。 warningのみでfailed/resolution_failedが0件のケースでは付与しない(警告はパイプライン失敗を伴わないため)。 MCPのrunツールも同じ生成関数(pyfltr.output.jsonl.build_summary_guidance)からRunResult.guidanceを返す
  • warningレコードのhint: 個別の警告の対処。警告の本文(msg)とは別のキーで持つ。 テキスト表示のwarnings節・stderr・MCPの応答ではpyfltr.warnings_.format_warning_textが本文に続けて 対処を連結した文字列を用い、どの出力先でも対処が欠けないようにする

command.hints / hint_urls 集約

修正ヒントとルールドキュメントURLはdiagnostic本体に含めず、commandレコードのhints / hint_urls辞書に集約する。 キーはrule IDで、形式は<plugin>/<rule>または単一rule名。 textlintコマンドの場合のみ、hintsにmessages[].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"設定またはツール固有の一時障害分類によって失敗結果が status="warning"に格下げされたものを集計する(パイプライン全体exit codeには影響しない)。 ツール起動自体に失敗したケース(resolution_failed / timeout_exceeded)はseverityの影響を受けない。 実行時警告(kind:"warning"レコード)の件数はcommands_summaryの外の条件付きキーwarningsで表し、 1件以上のときだけ出力する。commands_summary.needs_action.warningとは集計対象が異なる。

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

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

  • 必須キー: kind → exit → completion → files_reached → completed_commands → incomplete_commands → commands_summary → diagnostics
  • 条件付きキー: warnings → guidance → applied_fixes → fully_excluded_files → missing_targets

コマンド単位の集計(statusカテゴリ別件数およびコマンド総数 total)は commands_summary 配下に集約し、 total は no_issues / needs_action の末尾へ置く。 指摘総件数 diagnostics はコマンド単位の集計ではなく commands_summary の外に並べる。 完了判定の4キーは終了コードと並ぶ結論としてexitの直後に置く(定義は完了判定を参照)。

テスター失敗時の生出力併記

テスター(command_type == "tester")はリンター・フォーマッターと異なり、診断1件が assertの等価比較や例外メッセージの要約に留まり、失敗原因の特定に生出力(スタックトレース・ xdistワーカークラッシュの詳細等)を要することが多い。このためtext/TUI/JSONLの3出力方式 いずれも、テスター失敗時は診断一覧の有無にかかわらず生出力を併記する設計とする (linter・formatterは従来どおり診断が有れば生出力を省略する)。

CI(GitHub Actions)では--output-format=github-annotations使用時、text出力の生出力 併記部分に::を含む行があるとワークフローコマンドとして誤解釈される恐れがある。 そのためGitHub公式の::stop-commands::<token> / ::<token>::で該当ブロックを囲む (pyfltr/output/render.pyの_write_raw_output)。

CIワークフロー側では実行アーカイブ(PYFLTR_CACHE_DIR配下)をif: failure()条件で actions/upload-artifactによりジョブ成果物として保存する構成を推奨する。生出力併記だけでは JSONL側のsmart truncationで長大な出力が切り詰められる場合があり、アーカイブ成果物化により 切り詰め分も含めた全文を事後参照できる。

heartbeat出力

heartbeatは長時間実行中の生存確認をJSONLストリームへ追加する。 発火条件とレコード構築の仕様は各実装モジュールのdocstringに集約する。 関連する実装はpyfltr/output/jsonl.py・pyfltr/command/core_.py・pyfltr/cli/pipeline.pyに配置する。 利用者向けの条件とレコードの読み方はCLIコマンドを参照。

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等)の双方を取りこぼさない狙いがある。

--quiet指定時はheaderレコードをrun_id・commands・filesへ縮約する。 run-for-agentは常に既定有効で、エージェント検出用の環境変数がある環境では実行系全体で既定有効となる。 成功時commandレコードも抑止する。 詳細SSOTはbuild_command_linesと_build_header_recordのdocstring。

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"

診断位置の契約はpyfltr.diagnostics.ErrorLocationが持つ。 lineとcolは1起点、end_lineは診断範囲の最終行を含む値、end_colは1起点・終端排他とする。 ツールが返す終了位置は範囲末尾の次の位置を指す場合があり、範囲が行末で終わると次行が渡される。 終了列が次行の先頭(1起点で1)を指し、終了行が開始行より後にある場合、 _to_inclusive_end_positionが終了行を直前の行へ補正し、診断範囲の最終行として格納する。 補正前の終了列は次行上の値であり、直前行の終端列をツール出力から算出できないため、 終了行を補正する場合はend_colを省略する。 行と列を1つの関数が組で返すため、開始行の渡し違いで両者が別の行を指す事態が成立しない。 各出力形式は補正済みの値を消費するため、形式側で行の包含・排他を変換しない。

列を出力してよいかの判定はpyfltr.output.positions.is_publishable_columnへ集約する。 SARIFとGitHub Annotationsが同じ関数を用いるため、両形式の列ガードは常に一致する。 Code Qualityは列を出力しないため対象の関数を使わない。

SARIFのregionは正規化済みの開始位置に加え、診断が保持する終了行・終了列を endLine・endColumnへ反映する。列は1起点・終端排他とする。

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

Code Qualityのlocationはlines.beginに加え、診断が終了行を保持する場合にlines.endを出力する。 Code Climate仕様の行範囲は両端を含むため、begin補正後の値より前の終了行は範囲として成立せず出力しない。 列は開始列・終了列とも出力しない。GitLabの取り込み実装はlocation.lines.beginと location.positions.begin.lineだけを参照し、列を保持するフィールドを読まないためである (linesが優先され、欠落時のみpositionsへフォールバックする)。 列を伝えるにはlinesをpositionsへ置き換える必要があり、読まれない情報のために 開始行の表現をフォールバック側へ移すことになるため採らない。 lines.endもGitLabは読まないが、Code Climate仕様が定める行範囲の表現であり、 lines.beginの表現を変えずに追加できる。

GitHub Annotationのプロパティはfile / line / endLine / col / endColumn / titleの順に積む。 終了位置は診断が保持する場合のみ出力する。列は1起点・終端排他の値を無変換で渡す。 GitHubの仕様は列の起点だけを定め終端の包含・非包含を明示しないため、SARIFと同じ保持値を 渡して形式間の突合を成立させる。 生ログ視認用に本文へ前置する{file}:{line}[:{col}]:はログビューアーでの判読を目的とする 別契約であり、列ガードと終了位置を適用しない。 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.output.logging_.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.output.logging_.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未設定(構造化出力は発生しない)

JSONL出力へwarningレコードとして配送した警告はroot loggerからstderrへ重複出力しない。 構造化出力への書き込みが完了しなかった警告は保留スコープの終了時にstderrへ出力し、警告自体を失わない。 MCPのrunが内部一時ファイルへ生成するJSONLは利用者への配送ではないため、配送済みとして扱わない。 MCPのrunは代わりに、応答のwarningsへ含めた警告を配送済みとして扱う。 配送保留スコープを応答の組み立てまで広げ、例外で応答に至らなかった警告だけをstderrへ出力する。 stderrへ出力する警告は、hintを持つ場合に本文へ対処を連結した形で出力する。

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

実行アーカイブとファイル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向け出力(output/jsonl.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/state/cache.pyモジュール冒頭docstringを参照。 formatter・tester・依存型linter・外部参照linter・階層型設定linterの5分類を扱う。 --config/--ignore-path検知時の安全側無効化も同所に記載する。

grepの適応的出力

pyfltr.grep_.adaptiveは全マッチを受け取り、text・json・jsonl・MCPごとの実際の直列化長を測って結果表現を選ぶ。 結果部分の既定予算は10,000文字とし、header・warning・summary・guidanceは予算へ含めない。 ファイル名や拡張子には依存せず、ファイル単位の件数表示へ切り替えたときの削減量が大きい順に高密度ファイルを縮約する。 予算内の低密度ファイルはマッチ本文を維持し、全ファイルを件数だけへ縮約する前に混在形式を選ぶ。 混在形式でも収まらない場合は、全ファイルの件数表示、各ファイルからラウンドロビンで代表マッチを選ぶ形式の順に縮約する。

CLIのエージェント検出環境とMCPでは自動縮約を既定値として有効にする。 通常のCLI表示と、明示された件数・プレビュー・集計指定は従来の意味を維持する。 JSONLは選択したoutput_modeをheaderへ確定してから出力する必要があるため、grepに限り全検索結果をバッファリングする。 replace --from-grepは対象集合の完全性を必要とするため、output_modeがfull以外のJSONLを拒否する。

CLIとMCPの設計判断

サブコマンドとargparse

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

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

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

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

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

登録・引数解析・表示はpyfltr/cli/runs.py、保存結果の収集とrun_id解決はpyfltr/state/runs.pyに分ける。 cli/main.pyはconfig/generate-shell-completionと同じ「非実行系サブパーサー」として サブパーサー登録とディスパッチのみを行い、出力ロジックは持たない。

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

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

アーカイブの保存キーはツール名固定のため、同一ツール名のfixステージと通常ステージは 通常ステージで上書きされる。 fixステージが失敗した場合は通常ステージの結果へ統合した1件を保存するため、 show-runからfixステージの失敗と出力を取得できる(本書「実行パイプライン」を参照)。 show-runは各ツールの最終保存結果のみを参照可能で、ステージ別保存への拡張は対象外とする。

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

--from-run

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

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

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

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

--commandsと有効化の関係

--commandsで指定されたコマンドは、起点configで有効化されていないものはパイプラインから除外される。 モノレポではsubproject_aware=Trueツールに限りサブプロジェクトいずれかで有効化されていれば実行対象に含める (is_command_enabled_anywhere)。 除外自体は仕様どおりだが、指定したコマンドが暗黙に除外される挙動を利用者が誤解しないよう配慮する。 args.commands明示指定時のみ差集合を計算する。 warnings_.emit_warning(source="commands", ...)で警告発行する。 --commands未指定時(暗黙全展開)はnoiseとなるため警告しない。

エイリアス名で指定した場合は、展開結果に有効化済みコマンドが1件以上あれば警告しない。 エイリアスは該当するものを実行する意味で使い、言語構成によって一部のみ有効となるのが正常な状態のためである。 例えば推奨ガイド(docs/guide/recommended-nonpython.md)は監査ツールの定期実行に--commands=auditを案内している。 JavaScriptのみのプロジェクトではuv-audit等が未有効であり、未有効のコマンドを警告すると案内どおりの操作が必ず警告を伴う。 展開結果が全て未有効の場合は実行対象が空になるため警告を残す。 個別コマンド名の指定はエイリアスとの併記の有無を問わず警告対象とする。 判定本体はpyfltr/cli/command_selection.pyのcompute_unmet_commandsのdocstringをSSOTとする。

MCPサーバー

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

提供ツール構成

読み取り系ツール(list_runs・show_run・show_run_diagnostics・show_run_output)・ 実行系1ツール(run)を公開する。 grep/replace系ツール(grep・replace・replace_undo・replace_history)と 情報/設定系2ツール(command_info・config)を加え、公開ツールは計11件となる。 実行系はrunのmodeでci/run/fastを選択し、同一の入力・出力モデルで扱う。

ツール名は対応するCLIサブコマンド名と一致させる。 利用者とエージェントが同じ操作を2つの名前で覚える状態を避けるため、 実行系ツールはCLIのrunサブコマンドと同じrunとし、CLI側の互換用別名run-for-agentをツール名へ持ち込まない。 区切り文字だけはCLIのハイフン形式と異なりアンダースコア形式(list_runs/show_run等)とする。 ハイフンはPythonの@mcp.tool()名として非推奨のため。

応答スキーマの方針

実行アーカイブの保存メタデータを構造化して返すツールは、値の長さがチェック対象ファイル数に比例する項目を応答へ含めない。 show_run_diagnosticsのcommand_metaはtool.jsonからcommandlineを除いた項目を返す。 対象ファイルを引数へ展開するツールではcommandlineの長さが対象ファイル数に比例し、 大規模な対象では応答の大半を占めて診断本体の読み取りを妨げるためである。 完全な引数列はpyfltr show-run <run_id> --commands=<name>と実行アーカイブのtool.jsonから取得する。

保存ファイルの全文取得を目的とするshow_run_outputは本方針の対象外とし、output.logをそのまま返す。 show_runが返すmetaも対象外とする。 meta.jsonのargvは利用者が起動時に渡した引数を保持し、 pyfltrが対象ファイルへ展開した結果を含まないためである。

MCPライブラリ

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

stdio隔離

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

  1. 起動直後にroot loggerの出力先をstderrへ強制する
  2. runツール内ではrun_pipelineにforce_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の実装の流れ

公開ツールはpyfltr.cli.mcp_transport.isolate_toolを経由し、要求ごとに pyfltr.cli.mcp_workerを起動する。同期のファイル走査やツール実行を受信ループから分離し、 処理中もpingとキャンセル通知を受信できるようにする。 警告収集とloggerの出力先もworker固有となり、他要求へ混入しない。 キャンセル時は起動handleが返したworkerとその子孫だけを停止・回収してから取り消しを返す。 ツールが別のprocess groupを作成するため、workerのgroupだけでなく子孫のPIDを照会して停止する。

worker内のrunは共通のRunOptionsを構築し、run_pipelineを直接呼び出す。 CLIは公開引数を同じ型へ変換する。既定値は共通のオプション型、 commandsの平坦化はflatten_commands_argへ委ねる。 未知コマンドの検証はvalidate_commands、CLI指定による設定上書きはapply_cli_overridesを使い、 CLIと同じ解決処理を通す。 run(sys_args=[...])経由でargparseに渡す案ではエラーメッセージのstderr出力制御が困難で、 MCPツール側でのエラー整形ができないため不採用。 workerへはJSONで引数を渡し、既存のPydanticモデルへ結果を復元する。 CLI引数への変換は行わないため、公開スキーマとCLIの解析・出力形式を二重に管理しない。 別スレッドだけで実行する案は、共有loggerの排他と、mise/gitの直接起動を含む 実行固有の停止登録簿の伝播が必要となるため採用しない。 worker方式は起動費用を持つが、共有状態を分離し、子孫をまとめて停止できる。

work_dirはos.chdir()でプロセスのcwdを変更せず、設定探索では load_config(config_dir=work_dir)、実行パイプラインではrun_pipeline(start_cwd=work_dir)へ渡す。 プロセスグローバルなcwdに干渉しないため、ThreadPoolExecutorによるツールの並列実行や MCPクライアントからの並行ツール呼び出しでも実行起点を呼び出し単位で維持できる。

診断のファイルパスは解析境界でstart_cwd(work_dir)を基準に相対化する(本書「モノレポ対応」を参照)。 pyfltr.paths.to_cwd_relative()はプロセスのcwdだけを相対化の基準とするため、 診断以外でこの関数を経由する値はwork_dir指定時に絶対パスで返る場合がある。

run_pipeline()戻り値

run_pipeline()の戻り値はPipelineOutcome(exit_code・run_id・completion・resultsの名前付きタプル)とする。 run_idはアーカイブ無効時・early exit時にNone、それ以外では採番済みULIDが入る。 completionは全ての終了箇所で確定した完了判定で、MCPのrunはその6項目をそのまま応答へ転記する。

only_failed有効時に「直前runなし」「失敗ツールなし」「対象ファイル交差が空」「from_runの解決失敗」 「アーカイブの読み取り失敗」のいずれかに該当した場合、run_pipelineはearly exit(exit_code=0・run_id=None)を返す。 このときrunはエラーではなく「実行スキップ」を返し、skipped_reasonには run_pipelineがsource="only-failed"で発行した警告の本文と対処を入れる。固定文は返さない。

skipped_reasonはearly exit以外でも値を持つ。commandsへ指定したツールが設定で無効化されて実行されなかった場合、 tool_runはrun_pipelineがsource="commands"で発行した警告から理由を組み立てて同フィールドへ返す。 無効化の判定はrun_pipelineのis_command_enabled_anywhereとcompute_unmet_commandsが担い、MCP側では再判定しない。

MCPツールは冒頭でpyfltr.warnings_.clear()を呼び、対象の呼び出しが発行した警告だけを応答の入力にする。 run_pipeline自身は警告を初期化せず、初期化はpyfltr/cli/pipeline.pyのrun()内部実装が担うため、 run_pipelineを直接呼ぶMCPの処理では呼び出し側が初期化する。

resultsはキャッシュから復元した結果を含む。MCPはアーカイブを読み直さず、この結果から応答を構築する。

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