コンテンツにスキップ

アーキテクチャ概要

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-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・prek設定)
  • run_id生成: python-ulid
  • MCP同梱: mcpplatformdirs
  • プロセス判定: psutilgit commit経由起動を親系列で検出してMM状態ガイダンスを出力する用途)

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

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"とする。 対象はpython = trueゲートで有効化されるruff-format / ruff-check / mypy / pylint / pyright / ty / uv-sort / pytestの8ツールに、 既定無効のbanditを加えた9ツールとする。 "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.tomlCargo.toml*.csproj*.sln)を検出した場合、 サブプロジェクト単位でツールを分割実行する。 検出ロジック・分類・uv workspace解釈・uv.lock 親方向探索は pyfltr/command/subprojects.py に集約する。

設計上の判断:

  • 検出はマーカー(固定ファイル名pyproject.tomlCargo.toml、globパターン*.csproj*.sln)の存在で判定する([tool.pyfltr]の有無は問わない)。これにより既存モノレポを 移行する際、各サブプロジェクトに設定追加を強いない。マーカー一覧は _SUBPROJECT_MARKER_NAMES_SUBPROJECT_MARKER_GLOBSpyfltr/command/subprojects.py)を 典拠とする。package.jsonはプロジェクト境界を持たない汎用ファイルとして 誤検出リスクが高いため対象外とする
  • 検出結果が0件または1件の場合はモノレポモード非適用とし、従来通り単一実行する
  • ツール×サブプロジェクトの分割は CommandInfo.subproject_aware フラグで個別制御する。 プロジェクトローカル設定・モジュール解決・lockfileをcwd起点で読むツール (Python系・JS系・Rust系・.NET系のlinter/testerなど)は既定True。 リポジトリ単位で動作するツール(typosshellcheckshfmtpre-commitprek)は既定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: <相対パス> の区切り行のみ挿入する
  • subprocess.Popen へのcwd切り替えは引数渡しのみで実現する。os.chdir() を サブプロジェクトループで使うとプロセスcwdが並列実行中の他ツールへ干渉するため、 cwd依存処理(mise・git・ファイル走査・snapshot等)は明示引数で起点cwdを取得する形に統一する
  • uv workspace のmemberではcwd直下に uv.lock がない場合のみworkspace rootまで 親方向探索を許可する。無関係な祖先ディレクトリは越境しない
  • サブプロジェクトループ全体を警告重複の抑止スコープとする。 スコープ内ではemit_warningの同一sourcemessage組と、 add_filtered_direct_fileの同一pathreason組を1件だけ保持する。 独立project間で同一文言を発行する既存仕様と、 不在ターゲット件数に基づく終了判定を維持するため、スコープ外は重複を許容する

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

  1. 同じツールを有効化したサブプロジェクトを2件以上用意する
  2. 起点と複数のサブプロジェクトで、同一のsourcemessageを持つ警告を意図的に発生させる
  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の不正値を正常値で上書きした場合は起点の結果に警告組が現れず、 不正値を上書きしない複数サブプロジェクト間の重複を防げない

ExecutionParams.targetsCommandResult.target_filesの下流経路

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

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

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

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

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

モジュール分割の方針

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等のフィールドが追加された際の追従コストを抑える狙い。

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

テスター(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/cli/render.py_write_raw_output)。

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

ファイル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検知時の安全側無効化も同所に記載する。

heartbeat出力

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

出力フォーマット

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

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

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

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

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

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の影響を受けない。 実行時警告(kind:"warning"レコード)の件数はcommands_summaryの外の条件付きキーwarningsで表し、 1件以上のときだけ出力する。commands_summary.needs_action.warningとは集計対象が異なる。

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

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

  • 必須キー: kindexitcommands_summarydiagnostics
  • 条件付きキー: warningsguidanceapplied_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等)の双方を取りこぼさない狙いがある。

--quiet指定時はheaderレコードをrun_idcommandsfilesへ縮約する。 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.command.error_parser.ErrorLocationが持つ。 linecolは1起点、end_lineは診断範囲の最終行を含む値、end_colは1起点・終端排他とする。 ツールが返す終了位置は範囲末尾の次の位置を指す場合があり、範囲が行末で終わると次行が渡される。 _to_inclusive_end_positionが終了列の行頭判定で当該分を補正し、格納時点で最終行へ揃える。 補正前の終了列は次行上の値であり、直前行の終端列をツール出力から算出できないため、 終了行を補正する場合はend_colを省略する。 行と列を1つの関数が組で返すため、開始行の渡し違いで両者が別の行を指す事態が成立しない。 各出力形式は補正済みの値を消費するため、形式側で行の包含・排他を変換しない。

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

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

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

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

--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.pycompute_unmet_commandsのdocstringをSSOTとする。

MCPサーバー

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

提供ツール構成

読み取り系4ツール(list_runsshow_runshow_run_diagnosticsshow_run_output)・ 実行系1ツール(run_for_agent)を公開する。 grep/replace系4ツール(grepreplacereplace_undoreplace_history)と 情報/設定系2ツール(command_infoconfig)を加え、公開ツールは計11件となる。 実行系はrun_for_agentmodeci/run/fastを選択し、同一の入力・出力モデルで扱う。

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

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_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を直接呼び出す。 Namespaceの組み立てはMCP側に残す一方、サブコマンド既定値はapply_subcommand_defaultscommandsの平坦化はflatten_commands_argへ委ねる。 未知コマンドの検証はvalidate_commands、CLI指定による設定上書きはapply_cli_overridesを使い、 CLIと同じ解決経路を通す。 run(sys_args=[...])経由でargparseに渡す案ではエラーメッセージのstderr出力制御が困難で、 MCPツール側でのエラー整形ができないため不採用。 外部プロセス起動(subprocess.run(["pyfltr", "run-for-agent", ...]))案も検討した。 プロセス管理・PYFLTR_CACHE_DIR伝搬・TERMシグナル・テスト安定性の面で同一プロセス方式より不利のため不採用。

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

work_dir指定時は診断のファイルパスが絶対パスで返る場合がある。 pyfltr.paths.to_cwd_relative()はプロセスのcwdだけを相対化の基準とし、 work_dirを基準ディレクトリとして注入する経路を持たないためである。

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を誤って拾うリスクがあるため戻り値経由とした。