pyfltr 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {pyfltr-2.2.0 → pyfltr-2.3.0}/CLAUDE.md +2 -10
  2. {pyfltr-2.2.0 → pyfltr-2.3.0}/PKG-INFO +2 -2
  3. {pyfltr-2.2.0 → pyfltr-2.3.0}/README.md +1 -1
  4. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/usage.md +34 -6
  5. {pyfltr-2.2.0 → pyfltr-2.3.0}/mkdocs.yml +6 -7
  6. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/cli.py +17 -3
  7. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/command.py +1 -1
  8. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/error_parser.py +67 -19
  9. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/llm_output.py +101 -22
  10. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/main.py +54 -12
  11. pyfltr-2.3.0/pyfltr/shell_completion.py +214 -0
  12. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/error_parser_test.py +66 -10
  13. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/output_format_test.py +183 -21
  14. pyfltr-2.3.0/tests/shell_completion_test.py +111 -0
  15. pyfltr-2.2.0/.claude/settings.json +0 -13
  16. {pyfltr-2.2.0 → pyfltr-2.3.0}/.claude/agents/error-parser-reviewer.md +0 -0
  17. {pyfltr-2.2.0 → pyfltr-2.3.0}/.claude/agents/tool-compat-checker.md +0 -0
  18. {pyfltr-2.2.0 → pyfltr-2.3.0}/.claude/skills/pyfltr-add-tool/SKILL.md +0 -0
  19. {pyfltr-2.2.0 → pyfltr-2.3.0}/.editorconfig +0 -0
  20. {pyfltr-2.2.0 → pyfltr-2.3.0}/.gitattributes +0 -0
  21. {pyfltr-2.2.0 → pyfltr-2.3.0}/.github/workflows/ci.yaml +0 -0
  22. {pyfltr-2.2.0 → pyfltr-2.3.0}/.github/workflows/docs.yaml +0 -0
  23. {pyfltr-2.2.0 → pyfltr-2.3.0}/.github/workflows/release.yaml +0 -0
  24. {pyfltr-2.2.0 → pyfltr-2.3.0}/.gitignore +0 -0
  25. {pyfltr-2.2.0 → pyfltr-2.3.0}/.gitmessage +0 -0
  26. {pyfltr-2.2.0 → pyfltr-2.3.0}/.markdownlint-cli2.yaml +0 -0
  27. {pyfltr-2.2.0 → pyfltr-2.3.0}/.npmrc +0 -0
  28. {pyfltr-2.2.0 → pyfltr-2.3.0}/.pre-commit-config.yaml +0 -0
  29. {pyfltr-2.2.0 → pyfltr-2.3.0}/.pylintrc +0 -0
  30. {pyfltr-2.2.0 → pyfltr-2.3.0}/.python-version +0 -0
  31. {pyfltr-2.2.0 → pyfltr-2.3.0}/.textlintrc.yaml +0 -0
  32. {pyfltr-2.2.0 → pyfltr-2.3.0}/.vscode/extensions.json +0 -0
  33. {pyfltr-2.2.0 → pyfltr-2.3.0}/.vscode/settings.json +0 -0
  34. {pyfltr-2.2.0 → pyfltr-2.3.0}/LICENSE +0 -0
  35. {pyfltr-2.2.0 → pyfltr-2.3.0}/Makefile +0 -0
  36. {pyfltr-2.2.0 → pyfltr-2.3.0}/cliff.toml +0 -0
  37. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/.markdownlint-cli2.yaml +0 -0
  38. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/development/development.md +0 -0
  39. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/development/index.md +0 -0
  40. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/configuration-tools.md +0 -0
  41. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/configuration.md +0 -0
  42. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/custom-commands.md +0 -0
  43. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/index.md +0 -0
  44. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/recommended-nonpython.md +0 -0
  45. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/guide/recommended.md +0 -0
  46. {pyfltr-2.2.0 → pyfltr-2.3.0}/docs/index.md +0 -0
  47. {pyfltr-2.2.0 → pyfltr-2.3.0}/mise.toml +0 -0
  48. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/__init__.py +0 -0
  49. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/__main__.py +0 -0
  50. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/config.py +0 -0
  51. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/executor.py +0 -0
  52. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/precommit.py +0 -0
  53. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/ui.py +0 -0
  54. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyfltr/warnings_.py +0 -0
  55. {pyfltr-2.2.0 → pyfltr-2.3.0}/pyproject.toml +0 -0
  56. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/__init__.py +0 -0
  57. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/cli_test.py +0 -0
  58. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/command_test.py +0 -0
  59. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/config_test.py +0 -0
  60. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/conftest.py +0 -0
  61. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/executor_test.py +0 -0
  62. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/llm_output_test.py +0 -0
  63. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/main_test.py +0 -0
  64. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/precommit_test.py +0 -0
  65. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/ui_test.py +0 -0
  66. {pyfltr-2.2.0 → pyfltr-2.3.0}/tests/warnings_test.py +0 -0
  67. {pyfltr-2.2.0 → pyfltr-2.3.0}/uv.lock +0 -0
@@ -6,7 +6,7 @@
6
6
  - `make update-actions`: GitHub Actionsのハッシュピン更新のみ(mise経由でpinact実行)
7
7
  - テストコードは`pyfltr/xxx_.py`に対して`tests/xxx_test.py`として配置する
8
8
  - 実行パイプラインの構造: `run_pipeline`(main.py)がTUI/非TUI分岐の最上位関数。パイプライン共通の前処理(ファイル展開など)はこの関数内でTUI起動前に実行する
9
- - コミット前の検証方法: `uv run pyfltr run-for-agent | tail -30`
9
+ - コミット前の検証方法: `uv run pyfltr run-for-agent`
10
10
  - ドキュメントなどのみの変更の場合は省略可(pre-commitで実行されるため)
11
11
  - テストコードの単体実行なども極力 `uv run pyfltr run-for-agent <path>` を使う(pytestを直接呼び出さない)
12
12
  - 詳細な情報などが必要な場合に限り `uv run pytest -vv <path>` などを使用
@@ -15,13 +15,5 @@
15
15
 
16
16
  - `uv run mkdocs build --strict`でリンク・nav整合性を検証(ただし日本語アンカーリンク`#見出し日本語`はMkDocs TOCで解決できずINFO通知のみで`--strict`でも検知されないため手動確認要)
17
17
  - `docs/guide/index.md`の対応ツール一覧と`mkdocs.yml`内llmstxt `markdown_description`の「対応ツール」節は人手同期(SSOT化しない運用)
18
+ - `mkdocs.yml`内llmstxt `markdown_description`にはLLMが利用する際に有用な情報のみ記載する(`run-for-agent`サブコマンド、主要オプションなど)。LLMにとって不要な情報はdocs側をSSOTとし、多重管理を避ける
18
19
  - ドキュメント構成変更時は`docs/development/development.md`の「READMEとdocsの役割分担」節を先に参照
19
-
20
- ## 関連ドキュメント
21
-
22
- - @README.md
23
- - @docs/index.md
24
- - @docs/guide/index.md
25
- - @docs/development/index.md
26
- - @docs/development/development.md
27
- - ドキュメント追加時は `mkdocs.yml` の `nav` を更新要
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfltr
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Python Formatters, Linters, and Testers Runner.
5
5
  Project-URL: Homepage, https://github.com/ak110/pyfltr
6
6
  Author-email: "aki." <mark@aur.ll.to>
@@ -54,7 +54,7 @@ Description-Content-Type: text/markdown
54
54
  - 設定の集約: `pyproject.toml`に寄せた統一設定
55
55
  - 除外指定(exclude)の書式差をツール間で吸収
56
56
  - 自動修正系ツール(ruff format・prettierなど)を修正と失敗扱いの両立で実行
57
- - LLMエージェント向けJSON Lines出力(`--output-format=jsonl`)に対応
57
+ - LLMエージェント向けJSON Lines出力(`pyfltr run-for-agent`・`PYFLTR_OUTPUT_FORMAT`環境変数・`--output-format=jsonl`)に対応
58
58
 
59
59
  ## インストール
60
60
 
@@ -12,7 +12,7 @@
12
12
  - 設定の集約: `pyproject.toml`に寄せた統一設定
13
13
  - 除外指定(exclude)の書式差をツール間で吸収
14
14
  - 自動修正系ツール(ruff format・prettierなど)を修正と失敗扱いの両立で実行
15
- - LLMエージェント向けJSON Lines出力(`--output-format=jsonl`)に対応
15
+ - LLMエージェント向けJSON Lines出力(`pyfltr run-for-agent`・`PYFLTR_OUTPUT_FORMAT`環境変数・`--output-format=jsonl`)に対応
16
16
 
17
17
  ## インストール
18
18
 
@@ -70,6 +70,30 @@ pyfltr generate-config
70
70
  設定ファイルの雛形を標準出力に書き出す。`[tool.pyfltr]`セクションに貼り付けて利用する。
71
71
  このサブコマンドは他のオプションやターゲット指定を受け付けず、設定出力だけを行う。
72
72
 
73
+ ### サブコマンド: generate-shell-completion
74
+
75
+ ```shell
76
+ pyfltr generate-shell-completion bash
77
+ pyfltr generate-shell-completion powershell
78
+ ```
79
+
80
+ シェル補完スクリプトを標準出力に書き出す。
81
+ 引数にシェル種別(`bash`または`powershell`)を指定する。
82
+
83
+ bashでの設定例:
84
+
85
+ ```shell
86
+ eval "$(pyfltr generate-shell-completion bash)"
87
+ ```
88
+
89
+ PowerShellでの設定例:
90
+
91
+ ```powershell
92
+ pyfltr generate-shell-completion powershell | Out-String | Invoke-Expression
93
+ ```
94
+
95
+ 永続化する場合はプロファイルに上記を追記する。
96
+
73
97
  ### `[files and/or directories ...]`
74
98
 
75
99
  対象を指定しなかった場合は、カレントディレクトリ(`.`)を指定した場合と同じ扱いとなる。
@@ -81,7 +105,7 @@ pyfltr generate-config
81
105
  - markdownlint / textlint: `*.md`
82
106
  - pytest: `*_test.py`
83
107
 
84
- ### `fast` / `run` / `ci`の動作の違いと自動修正(fixステージ)
108
+ ### `fast` / `run` / `run-for-agent` / `ci`の動作の違いと自動修正(fixステージ)
85
109
 
86
110
  各サブコマンドの主な違いを以下に示す(軽い順)。
87
111
 
@@ -176,14 +200,16 @@ CLIオプション`--output-format`が指定されている場合は環境変数
176
200
 
177
201
  ### jsonlスキーマ
178
202
 
179
- 出力は以下4種別のレコードからなる。`kind`フィールドでレコード種別を判別する。
203
+ 出力は以下5種別のレコードからなる。`kind`フィールドでレコード種別を判別する。
180
204
 
205
+ - `header`: 先頭1行。実行環境情報(`version` / `python` / `platform` / `cwd` / `commands` / `files`)
181
206
  - `warning`: pyfltrが検出した設定・実行時の警告(`source`で発生元を識別)
182
207
  - `diagnostic`: 個々の診断(`error_parser`対応ツールの抽出結果)
183
208
  - `tool`: 1ツール1レコードの実行メタ情報
184
209
  - `summary`: 最終1行、全体集計
185
210
 
186
211
  ```json
212
+ {"kind":"header","version":"1.25.0","python":"3.12.0 ...","executable":"/usr/bin/python3","platform":"linux","cwd":"/work","commands":["mypy","black"],"files":12}
187
213
  {"kind":"warning","source":"config","msg":"pre-commit が有効化されていますが、設定ファイルが見つかりません: .pre-commit-config.yaml"}
188
214
  {"kind":"diagnostic","tool":"mypy","file":"src/a.py","line":42,"col":5,"msg":"Incompatible return value type"}
189
215
  {"kind":"tool","tool":"mypy","type":"linter","status":"failed","files":12,"elapsed":0.8,"diagnostics":1,"rc":1}
@@ -191,7 +217,9 @@ CLIオプション`--output-format`が指定されている場合は環境変数
191
217
  {"kind":"summary","total":2,"succeeded":0,"formatted":1,"failed":1,"skipped":0,"diagnostics":1,"exit":1}
192
218
  ```
193
219
 
194
- 出力順は`warning`→`diagnostic`(file/line/col/command順)→`tool`(`pyproject.toml`の定義順)→`summary`(1行)で固定。
220
+ stdoutモード(`--output-file`未指定)では、先頭にheader行を出力し、各ツールの完了時にdiagnostic行+tool行を随時書き出す。
221
+ ツール間の出力順は完了順となり、最後にwarning行+summary行が続く。
222
+ ファイル出力時(`--output-file`指定)では、先頭にheader行、続いて`pyproject.toml`の定義順にツール単位でグルーピングし、先頭にwarning行、末尾にsummary行を配置する。
195
223
 
196
224
  `warning`レコードの`source`は`config`(設定ファイル不在など)/`tool-resolve`(ツール解決失敗)/`file-resolver`(対象ファイル選定時)/`git`(`git check-ignore`失敗)のいずれか。
197
225
 
@@ -212,7 +240,7 @@ LLMエージェントがpyfltrを活用する基本的な流れ:
212
240
  1. 全体実行でsummaryを確認する
213
241
 
214
242
  ```shell
215
- pyfltr run-for-agent | tail -30
243
+ pyfltr run-for-agent
216
244
  ```
217
245
 
218
246
  末尾のsummary行(`"kind":"summary"`)で`failed`の有無と`diagnostics`数を確認し、問題がなければ完了する。
@@ -220,11 +248,11 @@ LLMエージェントがpyfltrを活用する基本的な流れ:
220
248
  2. 問題があるツール/ファイルだけ個別に再実行する
221
249
 
222
250
  ```shell
223
- pyfltr run-for-agent --commands=mypy path/to/file.py | tail -30
251
+ pyfltr run-for-agent --commands=mypy path/to/file.py
224
252
  ```
225
253
 
226
254
  `--commands`で特定ツールに絞ることで出力量を抑えつつ、`diagnostic`行から修正対象のファイル・行番号・メッセージを取得する。
227
- 詳細が必要な場合に限り`--output-format=text`で再実行するなど、段階的に情報を掘り下げることも可能。
255
+ 詳細が必要な場合に限り`run`で再実行するなど、段階的に情報を掘り下げることも可能。
228
256
 
229
257
  ## pre-commitとの統合
230
258
 
@@ -38,9 +38,9 @@ plugins:
38
38
  - `pyfltr ci`: 全チェック。formatterによる変更も失敗と判定する(CI向け)
39
39
  - `pyfltr run`: 全チェック。fix段→formatter段→linter/tester段の順で、fix-args定義済みlinterの自動修正→formatter→残りの検査を実行する(ローカル向け)
40
40
  - `pyfltr fast`: 軽量チェック。`run`と同じ3段構成だが、mypy/pylint/pytest等の重いツールを除外(pre-commit向け)
41
- - `pyfltr generate-config`: pyproject.toml用の設定雛形を出力
41
+ - `pyfltr run-for-agent`: `pyfltr run --output-format=jsonl`のエイリアス。JSONL出力を既定にする(LLMエージェント向け)
42
42
 
43
- `--no-fix`で`run`/`fast`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
43
+ `--no-fix`で`run`/`fast`/`run-for-agent`のfix段を抑止できる。`ci`は副作用回避のため元々fix段を持たない。
44
44
 
45
45
  ## 対応ツール
46
46
 
@@ -67,19 +67,18 @@ plugins:
67
67
 
68
68
  ## LLMエージェント向け出力(`--output-format=jsonl`)
69
69
 
70
- `--output-format=jsonl`でJSON Lines形式の構造化出力が得られる。レコードは4種類。
70
+ `--output-format=jsonl`でJSON Lines形式の構造化出力が得られる。レコードは5種類。
71
71
 
72
+ - `header`: 先頭1行。実行環境情報(`version` / `python` / `platform` / `cwd` / `commands` / `files`)
72
73
  - `warning`: pyfltr が検出した設定・実行時の警告(`source` で発生元を識別)
73
74
  - `diagnostic`: 1診断1行(`tool` / `file` / `line` / `col` / `rule` / `severity` / `msg`)
74
75
  - `tool`: ツールごとの実行結果サマリ(`status` / `elapsed` / `diagnostics` / `rc`)
75
76
  - `summary`: 最終1行の全体集計(`succeeded` / `formatted` / `failed` / `skipped` / `diagnostics` / `exit`)
76
77
 
77
- 出力順は`warning`→`diagnostic`(file/line/col/command順)→`tool`(`pyproject.toml`定義順)→`summary`(1行)で固定。
78
-
79
- `--output-file`未指定時はstdoutへJSONLのみを書き、進捗ログやTUIは無音化される。指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
78
+ stdoutモード(`--output-file`未指定)の出力順は`header`→ツール完了順に`diagnostic`+`tool`→`warning`→`summary`。stdoutへJSONLのみを書き、進捗ログやTUIは無音化される。`--output-file`指定時はファイルへJSONLを書き、stdoutには従来どおりのテキスト出力を並行出力する(ローカル実行で進捗を追える)。
80
79
 
81
80
  ```shell
82
- pyfltr run-for-agent | tail -30
81
+ pyfltr run-for-agent
83
82
  ```
84
83
 
85
84
  `pyfltr run-for-agent`は`pyfltr run --output-format=jsonl`と等価のエイリアス。
@@ -29,6 +29,7 @@ def run_commands_with_cli(
29
29
  *,
30
30
  per_command_log: bool,
31
31
  include_fix_stage: bool = False,
32
+ on_result: typing.Callable[[pyfltr.command.CommandResult], None] | None = None,
32
33
  ) -> list[pyfltr.command.CommandResult]:
33
34
  """コマンドを実行する (非 TUI)。
34
35
 
@@ -40,6 +41,9 @@ def run_commands_with_cli(
40
41
  ``include_fix_stage=True`` のとき、fix-args 定義済みコマンドを先に ``--fix`` 付きで
41
42
  直列実行してから、formatter → linter/tester の順で通常実行に進む
42
43
  (``ruff check --fix → ruff format → ruff check`` と同じ 2 段階方式の一般化)。
44
+
45
+ ``on_result`` が指定されている場合、各コマンド完了時にコールバックを呼び出す。
46
+ JSONL stdoutモードでのストリーミング出力に使用する。
43
47
  """
44
48
  results: list[pyfltr.command.CommandResult] = []
45
49
  fixers, formatters, linters_and_testers = pyfltr.executor.split_commands_for_execution(
@@ -54,7 +58,10 @@ def run_commands_with_cli(
54
58
 
55
59
  # formatters を順序実行
56
60
  for command in formatters:
57
- results.append(_run_one_command(command, args, config, all_files, per_command_log=per_command_log))
61
+ result = _run_one_command(command, args, config, all_files, per_command_log=per_command_log)
62
+ results.append(result)
63
+ if on_result is not None:
64
+ on_result(result)
58
65
 
59
66
  # linters/testers を並列実行
60
67
  if len(linters_and_testers) > 0:
@@ -64,7 +71,10 @@ def run_commands_with_cli(
64
71
  for command in linters_and_testers
65
72
  }
66
73
  for future in concurrent.futures.as_completed(future_to_command):
67
- results.append(future.result())
74
+ result = future.result()
75
+ results.append(result)
76
+ if on_result is not None:
77
+ on_result(result)
68
78
 
69
79
  return results
70
80
 
@@ -130,6 +140,8 @@ def render_results(
130
140
  output_format: str = "text",
131
141
  output_file: pathlib.Path | None = None,
132
142
  exit_code: int = 0,
143
+ commands: list[str] | None = None,
144
+ files: int | None = None,
133
145
  warnings: list[dict[str, typing.Any]] | None = None,
134
146
  ) -> None:
135
147
  """実行結果を `成功コマンド → 失敗コマンド → summary` の順でまとめて出力する。
@@ -150,7 +162,9 @@ def render_results(
150
162
  warnings = warnings or []
151
163
 
152
164
  if output_format == "jsonl":
153
- pyfltr.llm_output.write_jsonl(ordered, config, exit_code=exit_code, destination=output_file, warnings=warnings)
165
+ pyfltr.llm_output.write_jsonl(
166
+ ordered, config, exit_code=exit_code, destination=output_file, commands=commands, files=files, warnings=warnings
167
+ )
154
168
  if output_file is None:
155
169
  return
156
170
 
@@ -121,7 +121,7 @@ _STRUCTURED_OUTPUT_SPECS: dict[str, tuple[str, _StructuredOutputSpec]] = {
121
121
  "pytest-tb-line": (
122
122
  "pytest",
123
123
  _StructuredOutputSpec(
124
- inject=["--tb=line"],
124
+ inject=["--tb=short"],
125
125
  conflicts=["--tb"],
126
126
  ),
127
127
  ),
@@ -427,28 +427,58 @@ def _parse_typos_jsonl(output: str) -> list[ErrorLocation]:
427
427
 
428
428
 
429
429
  def _parse_pytest(output: str) -> list[ErrorLocation]:
430
- """Pytest 出力をパース。--tb=line 形式の行番号付き出力を優先的に抽出する。"""
431
- # --tb=line 出力行を探す: /path/to/test.py:42: assert message
432
- tb_line_re = re.compile(rf"^(?P<file>{_FILE}):(?P<line>\d+):\s+(?P<message>.+)$", re.MULTILINE)
433
- # FAILURES セクション内の --tb=line 出力のみを対象にする
430
+ """Pytest出力をパース。--tb=short形式のトレースバックからプロジェクト内フレームを優先的に抽出する。"""
434
431
  failures_start = output.find("= FAILURES =")
435
432
  summary_start = output.find("short test summary info")
436
- if failures_start >= 0:
437
- end = summary_start if summary_start > failures_start else len(output)
438
- failures_section = output[failures_start:end]
439
- tb_results: list[ErrorLocation] = []
440
- for match in tb_line_re.finditer(failures_section):
441
- tb_results.append(
442
- ErrorLocation(
443
- file=_normalize_path(match.group("file")),
444
- line=int(match.group("line")),
445
- col=None,
446
- command="pytest",
447
- message=match.group("message").strip(),
448
- )
433
+ if failures_start < 0:
434
+ return _parse_with_pattern("pytest", output, _BUILTIN_PATTERNS["pytest"])
435
+
436
+ end = summary_start if summary_start > failures_start else len(output)
437
+ failures_section = output[failures_start:end]
438
+
439
+ # テスト単位のブロックに分割(`_ test_name _` 区切り)
440
+ block_re = re.compile(r"^_+ .+ _+$", re.MULTILINE)
441
+ block_starts = [m.end() for m in block_re.finditer(failures_section)]
442
+ if not block_starts:
443
+ return _parse_with_pattern("pytest", output, _BUILTIN_PATTERNS["pytest"])
444
+
445
+ # フレーム行: file:line: in func_name
446
+ frame_re = re.compile(rf"^(?P<file>{_FILE}):(?P<line>\d+): in .+$", re.MULTILINE)
447
+ # エラー行: E message
448
+ error_re = re.compile(r"^E\s+(?P<message>.+)$", re.MULTILINE)
449
+
450
+ results: list[ErrorLocation] = []
451
+ for i, start in enumerate(block_starts):
452
+ block_end = block_starts[i + 1] if i + 1 < len(block_starts) else len(failures_section)
453
+ block = failures_section[start:block_end]
454
+
455
+ # フレーム群から最後のプロジェクト内フレームを選択
456
+ frames = list(frame_re.finditer(block))
457
+ if not frames:
458
+ continue
459
+
460
+ chosen = frames[-1] # フォールバック: 最後のフレーム
461
+ for frame in reversed(frames):
462
+ if _is_project_path(_normalize_path(frame.group("file"))):
463
+ chosen = frame
464
+ break
465
+
466
+ # エラーメッセージ(先頭のE行)
467
+ error_match = error_re.search(block)
468
+ message = error_match.group("message").strip() if error_match else ""
469
+
470
+ results.append(
471
+ ErrorLocation(
472
+ file=_normalize_path(chosen.group("file")),
473
+ line=int(chosen.group("line")),
474
+ col=None,
475
+ command="pytest",
476
+ message=message,
449
477
  )
450
- if tb_results:
451
- return tb_results
478
+ )
479
+
480
+ if results:
481
+ return results
452
482
  # フォールバック: FAILED file::test_name パターン(line=0)
453
483
  return _parse_with_pattern("pytest", output, _BUILTIN_PATTERNS["pytest"])
454
484
 
@@ -576,3 +606,21 @@ def _normalize_path(file_path: str) -> str:
576
606
  return file_path
577
607
  return result.replace("\\", "/")
578
608
  return file_path.replace("\\", "/")
609
+
610
+
611
+ def _is_project_path(normalized_path: str) -> bool:
612
+ """正規化済みパスがプロジェクト内のファイルかを判定する。
613
+
614
+ 以下を全て満たす場合にプロジェクト内と見なす:
615
+ - 相対パスである(絶対パスはcwd外 = 標準ライブラリ等)
616
+ - ``..``で始まらない(uv管理Pythonの標準ライブラリ等)
617
+ - ``.venv/``で始まらない(仮想環境内サードパーティー)
618
+ - ``site-packages/``・``dist-packages/``を含まない(名前の異なる仮想環境内サードパーティー)
619
+ """
620
+ if pathlib.PurePosixPath(normalized_path).is_absolute():
621
+ return False
622
+ if normalized_path.startswith(".."):
623
+ return False
624
+ if normalized_path.startswith(".venv/"):
625
+ return False
626
+ return not ("site-packages/" in normalized_path or "dist-packages/" in normalized_path)
@@ -1,19 +1,26 @@
1
1
  """LLM 向け JSON Lines 出力。
2
2
 
3
3
  `--output-format=jsonl` で呼ばれ、CommandResult 群を LLM / エージェントが
4
- 読みやすいフラットな JSON Lines 形式 (diagnostic / tool / summary の 3 種別) に
4
+ 読みやすいフラットな JSON Lines 形式 (header / diagnostic / tool / summary の 4 種別) に
5
5
  変換して書き出す。
6
6
  """
7
7
 
8
+ import importlib.metadata
8
9
  import json
10
+ import os
9
11
  import pathlib
10
12
  import sys
13
+ import threading
11
14
  import typing
12
15
 
13
16
  import pyfltr.command
14
17
  import pyfltr.config
15
18
  import pyfltr.error_parser
16
19
 
20
+ # ストリーミング書き出し時に複数行(diagnostic行+tool行)をアトミックに出力するためのロック。
21
+ # 並列実行される linters/testers から同時にコールバックが呼ばれる可能性がある。
22
+ _write_lock = threading.Lock()
23
+
17
24
  # failed かつ diagnostics=0 のときに tool.message として載せる生出力のトリム上限。
18
25
  # 末尾 30 行を取り出し、さらに末尾 2000 文字に切り詰める。
19
26
  _MESSAGE_MAX_LINES = 30
@@ -21,45 +28,54 @@ _MESSAGE_MAX_CHARS = 2000
21
28
  _TRUNCATED_PREFIX = "... (truncated)\n"
22
29
 
23
30
 
31
+ def build_tool_lines(
32
+ result: pyfltr.command.CommandResult,
33
+ config: pyfltr.config.Config,
34
+ ) -> list[str]:
35
+ """1コマンド分のdiagnostic行+tool行をJSONL文字列のリストとして生成する。
36
+
37
+ diagnostic行はツール内でソートされる。
38
+ """
39
+ sorted_errors = pyfltr.error_parser.sort_errors(result.errors, config.command_names)
40
+ lines: list[str] = []
41
+ for error in sorted_errors:
42
+ lines.append(_dump(_build_diagnostic_record(error)))
43
+ lines.append(_dump(_build_tool_record(result, diagnostics=len(result.errors))))
44
+ return lines
45
+
46
+
24
47
  def build_lines(
25
48
  results: list[pyfltr.command.CommandResult],
26
49
  config: pyfltr.config.Config,
27
50
  *,
28
51
  exit_code: int,
52
+ commands: list[str] | None = None,
53
+ files: int | None = None,
29
54
  warnings: list[dict[str, typing.Any]] | None = None,
30
55
  ) -> list[str]:
31
- """CommandResult 群から JSONL 各行を生成する。
56
+ """CommandResult群からJSONL各行を生成する。
32
57
 
33
58
  出力順:
34
- 1. `warnings` が非空なら kind="warning" 行(先頭)
35
- 2. 全診断を (file, line, col, command 順) で昇順ソートした diagnostic
36
- 3. config.command_names の定義順に並べた tool 行
37
- 4. summary 1
59
+ 1. ``commands``と``files``が指定されていればkind="header"
60
+ 2. ``warnings``が非空ならkind="warning"
61
+ 3. ツール単位でdiagnostic行+tool行(``config.command_names``の定義順)
62
+ 4. summary行1行
38
63
 
39
- results は順序を問わない。内部で `config.command_names` 順にソートする。
40
- ``warnings`` は `pyfltr.warnings_.collected_warnings()` の返り値を想定する。
64
+ resultsは順序を問わない。内部で``config.command_names``順にソートする。
65
+ ``warnings``は``pyfltr.warnings_.collected_warnings()``の返り値を想定する。
41
66
  """
42
67
  ordered = sorted(results, key=lambda r: _command_index(config, r.command))
43
68
 
44
69
  lines: list[str] = []
45
70
 
71
+ if commands is not None and files is not None:
72
+ lines.append(_dump(_build_header_record(commands, files)))
73
+
46
74
  for warning in warnings or []:
47
75
  lines.append(_dump(_build_warning_record(warning)))
48
76
 
49
- all_errors: list[pyfltr.error_parser.ErrorLocation] = []
50
77
  for result in ordered:
51
- all_errors.extend(result.errors)
52
- sorted_errors = pyfltr.error_parser.sort_errors(all_errors, config.command_names)
53
- for error in sorted_errors:
54
- lines.append(_dump(_build_diagnostic_record(error)))
55
-
56
- diagnostic_counts: dict[str, int] = {}
57
- for error in all_errors:
58
- diagnostic_counts[error.command] = diagnostic_counts.get(error.command, 0) + 1
59
-
60
- for result in ordered:
61
- diagnostics = diagnostic_counts.get(result.command, 0)
62
- lines.append(_dump(_build_tool_record(result, diagnostics=diagnostics)))
78
+ lines.extend(build_tool_lines(result, config))
63
79
 
64
80
  lines.append(_dump(_build_summary_record(ordered, exit_code=exit_code)))
65
81
  return lines
@@ -78,6 +94,8 @@ def write_jsonl(
78
94
  *,
79
95
  exit_code: int,
80
96
  destination: pathlib.Path | None,
97
+ commands: list[str] | None = None,
98
+ files: int | None = None,
81
99
  warnings: list[dict[str, typing.Any]] | None = None,
82
100
  ) -> None:
83
101
  """JSONL を stdout もしくは指定ファイルに書き出す。
@@ -86,7 +104,7 @@ def write_jsonl(
86
104
  親ディレクトリを自動作成し、atomic write せず単純に上書きする
87
105
  (LLM 用途の使い捨てのため)。
88
106
  """
89
- lines = build_lines(results, config, exit_code=exit_code, warnings=warnings)
107
+ lines = build_lines(results, config, exit_code=exit_code, commands=commands, files=files, warnings=warnings)
90
108
  if destination is None:
91
109
  for line in lines:
92
110
  sys.stdout.write(line)
@@ -100,11 +118,72 @@ def write_jsonl(
100
118
  f.write("\n")
101
119
 
102
120
 
121
+ def write_jsonl_header(commands: list[str], files: int) -> None:
122
+ """header行をstdoutに書き出す(ストリーミングモード用)。
123
+
124
+ パイプライン開始直後、diagnostic行より前に1回だけ呼ぶ。
125
+ """
126
+ with _write_lock:
127
+ sys.stdout.write(_dump(_build_header_record(commands, files)))
128
+ sys.stdout.write("\n")
129
+ sys.stdout.flush()
130
+
131
+
132
+ def write_jsonl_streaming(
133
+ result: pyfltr.command.CommandResult,
134
+ config: pyfltr.config.Config,
135
+ ) -> None:
136
+ """1コマンド分のdiagnostic行+tool行をstdoutに即時書き出す。
137
+
138
+ ``_write_lock``取得下で書き出し+flushするため、並列実行されるlinters/testers
139
+ から呼ばれてもツール単位のグルーピングが崩れない。
140
+ """
141
+ lines = build_tool_lines(result, config)
142
+ with _write_lock:
143
+ for line in lines:
144
+ sys.stdout.write(line)
145
+ sys.stdout.write("\n")
146
+ sys.stdout.flush()
147
+
148
+
149
+ def write_jsonl_footer(
150
+ results: list[pyfltr.command.CommandResult],
151
+ *,
152
+ exit_code: int,
153
+ warnings: list[dict[str, typing.Any]] | None = None,
154
+ ) -> None:
155
+ """warning行+summary行をstdoutに書き出す。
156
+
157
+ ``results``は``_build_summary_record()``の集計に使用する。
158
+ """
159
+ with _write_lock:
160
+ for warning in warnings or []:
161
+ sys.stdout.write(_dump(_build_warning_record(warning)))
162
+ sys.stdout.write("\n")
163
+ sys.stdout.write(_dump(_build_summary_record(results, exit_code=exit_code)))
164
+ sys.stdout.write("\n")
165
+ sys.stdout.flush()
166
+
167
+
103
168
  def _dump(record: dict[str, typing.Any]) -> str:
104
169
  """JSON 1 行にシリアライズする。ensure_ascii=False + 区切り最短化でトークン効率を稼ぐ。"""
105
170
  return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
106
171
 
107
172
 
173
+ def _build_header_record(commands: list[str], files: int) -> dict[str, typing.Any]:
174
+ """実行環境の基本情報を header レコード dict として返す。"""
175
+ return {
176
+ "kind": "header",
177
+ "version": importlib.metadata.version("pyfltr"),
178
+ "python": sys.version,
179
+ "executable": sys.executable,
180
+ "platform": sys.platform,
181
+ "cwd": os.getcwd(),
182
+ "commands": commands,
183
+ "files": files,
184
+ }
185
+
186
+
108
187
  def _build_warning_record(entry: dict[str, typing.Any]) -> dict[str, typing.Any]:
109
188
  """警告 dict を warning レコード dict に変換する。"""
110
189
  return {