codetool-shell 0.1.1__py3-none-manylinux2014_aarch64.whl

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 (72) hide show
  1. codetool_shell/__init__.py +11 -0
  2. codetool_shell/api.py +59 -0
  3. codetool_shell/bin/linux-aarch64/codetool-shell-rust +0 -0
  4. codetool_shell/filters/__init__.py +14 -0
  5. codetool_shell/filters/build_compiler/__init__.py +7 -0
  6. codetool_shell/filters/build_compiler/detector.py +412 -0
  7. codetool_shell/filters/build_compiler/reducer.py +166 -0
  8. codetool_shell/filters/build_compiler/summary.py +617 -0
  9. codetool_shell/filters/ci_job_log/__init__.py +7 -0
  10. codetool_shell/filters/ci_job_log/detector.py +64 -0
  11. codetool_shell/filters/ci_job_log/reducer.py +99 -0
  12. codetool_shell/filters/ci_job_log/summary.py +243 -0
  13. codetool_shell/filters/diff/__init__.py +7 -0
  14. codetool_shell/filters/diff/detector.py +136 -0
  15. codetool_shell/filters/diff/reducer.py +308 -0
  16. codetool_shell/filters/generic_log/__init__.py +7 -0
  17. codetool_shell/filters/generic_log/detector.py +175 -0
  18. codetool_shell/filters/generic_log/reducer.py +99 -0
  19. codetool_shell/filters/generic_log/summary.py +161 -0
  20. codetool_shell/filters/git.py +514 -0
  21. codetool_shell/filters/html_cleanup/__init__.py +7 -0
  22. codetool_shell/filters/html_cleanup/detector.py +136 -0
  23. codetool_shell/filters/html_cleanup/reducer.py +27 -0
  24. codetool_shell/filters/html_cleanup/summary.py +422 -0
  25. codetool_shell/filters/json_payload/__init__.py +7 -0
  26. codetool_shell/filters/json_payload/detector.py +62 -0
  27. codetool_shell/filters/json_payload/reducer.py +81 -0
  28. codetool_shell/filters/json_payload/summary.py +233 -0
  29. codetool_shell/filters/listing/__init__.py +7 -0
  30. codetool_shell/filters/listing/detector.py +294 -0
  31. codetool_shell/filters/listing/reducer.py +30 -0
  32. codetool_shell/filters/log_template/__init__.py +7 -0
  33. codetool_shell/filters/log_template/constants.py +76 -0
  34. codetool_shell/filters/log_template/detector.py +331 -0
  35. codetool_shell/filters/log_template/reducer.py +78 -0
  36. codetool_shell/filters/log_template/template.py +280 -0
  37. codetool_shell/filters/log_template/types.py +21 -0
  38. codetool_shell/filters/opaque_payload/__init__.py +7 -0
  39. codetool_shell/filters/opaque_payload/detector.py +563 -0
  40. codetool_shell/filters/opaque_payload/reducer.py +142 -0
  41. codetool_shell/filters/opaque_payload/summary.py +61 -0
  42. codetool_shell/filters/package_manager/__init__.py +7 -0
  43. codetool_shell/filters/package_manager/detector.py +220 -0
  44. codetool_shell/filters/package_manager/reducer.py +110 -0
  45. codetool_shell/filters/package_manager/summary.py +172 -0
  46. codetool_shell/filters/pipeline.py +65 -0
  47. codetool_shell/filters/rg.py +250 -0
  48. codetool_shell/filters/system_output/__init__.py +7 -0
  49. codetool_shell/filters/system_output/detector.py +600 -0
  50. codetool_shell/filters/system_output/reducer.py +331 -0
  51. codetool_shell/filters/system_output/summary.py +164 -0
  52. codetool_shell/filters/table/__init__.py +7 -0
  53. codetool_shell/filters/table/detector.py +244 -0
  54. codetool_shell/filters/table/reducer.py +57 -0
  55. codetool_shell/filters/table/summary.py +37 -0
  56. codetool_shell/filters/test_runner/__init__.py +7 -0
  57. codetool_shell/filters/test_runner/ansi.py +80 -0
  58. codetool_shell/filters/test_runner/detector.py +409 -0
  59. codetool_shell/filters/test_runner/reducer.py +288 -0
  60. codetool_shell/filters/test_runner/summary.py +449 -0
  61. codetool_shell/filters/text.py +38 -0
  62. codetool_shell/filters/traceback/__init__.py +7 -0
  63. codetool_shell/filters/traceback/detector.py +209 -0
  64. codetool_shell/filters/traceback/reducer.py +141 -0
  65. codetool_shell/filters/traceback/summary.py +122 -0
  66. codetool_shell/filters/tree.py +59 -0
  67. codetool_shell/py.typed +0 -0
  68. codetool_shell/python_backend.py +38 -0
  69. codetool_shell/rust_backend.py +254 -0
  70. codetool_shell-0.1.1.dist-info/METADATA +152 -0
  71. codetool_shell-0.1.1.dist-info/RECORD +72 -0
  72. codetool_shell-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,11 @@
1
+ """Public Python interface for shell output text compression."""
2
+
3
+ from .api import compress_text
4
+ from .rust_backend import RustBackendError, RustBackendUnavailable, is_rust_available
5
+
6
+ __all__ = [
7
+ "RustBackendError",
8
+ "RustBackendUnavailable",
9
+ "compress_text",
10
+ "is_rust_available",
11
+ ]
codetool_shell/api.py ADDED
@@ -0,0 +1,59 @@
1
+ """Stable public API and backend selection for text compression."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .python_backend import compress_text_python
6
+ from .rust_backend import (
7
+ RustBackendError,
8
+ RustBackendUnavailable,
9
+ is_rust_available,
10
+ compress_text_rust,
11
+ )
12
+
13
+ BackendName = str
14
+
15
+
16
+ def compress_text(
17
+ text: str,
18
+ *,
19
+ max_blank_lines: int = 1,
20
+ backend: BackendName = "auto",
21
+ ) -> str:
22
+ """Compress plain shell-output text.
23
+
24
+ This initial scaffold applies one conservative, semantics-preserving pass:
25
+ normalize line endings, trim trailing whitespace on each line, and collapse
26
+ repeated blank lines. Future pipeline variants can layer on this API.
27
+ """
28
+
29
+ _validate_text(text)
30
+ max_blank_lines = _validate_max_blank_lines(max_blank_lines)
31
+
32
+ if backend == "python":
33
+ return compress_text_python(text, max_blank_lines=max_blank_lines)
34
+
35
+ if backend == "rust":
36
+ return compress_text_rust(text, max_blank_lines=max_blank_lines)
37
+
38
+ if backend == "auto":
39
+ if is_rust_available():
40
+ try:
41
+ return compress_text_rust(text, max_blank_lines=max_blank_lines)
42
+ except (RustBackendUnavailable, RustBackendError):
43
+ pass
44
+ return compress_text_python(text, max_blank_lines=max_blank_lines)
45
+
46
+ raise ValueError("backend must be one of: 'auto', 'python', 'rust'")
47
+
48
+
49
+ def _validate_text(text: str) -> None:
50
+ if not isinstance(text, str):
51
+ raise TypeError("text must be a string")
52
+
53
+
54
+ def _validate_max_blank_lines(max_blank_lines: int) -> int:
55
+ if isinstance(max_blank_lines, bool) or not isinstance(max_blank_lines, int):
56
+ raise TypeError("max_blank_lines must be an integer")
57
+ if max_blank_lines < 0:
58
+ raise ValueError("max_blank_lines must be greater than or equal to 0")
59
+ return max_blank_lines
@@ -0,0 +1,14 @@
1
+ """Dedicated shell-output filters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .git import compress_git_output
6
+ from .pipeline import TextFilter, apply_filter_pipeline
7
+ from .rg import compress_rg_output
8
+
9
+ __all__ = [
10
+ "TextFilter",
11
+ "apply_filter_pipeline",
12
+ "compress_git_output",
13
+ "compress_rg_output",
14
+ ]
@@ -0,0 +1,7 @@
1
+ """Build/compiler diagnostic shellion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .reducer import compress_build_compiler_diagnostics_output
6
+
7
+ __all__ = ["compress_build_compiler_diagnostics_output"]
@@ -0,0 +1,412 @@
1
+ """Detect conservative build/compiler diagnostic shapes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .summary import (
8
+ has_error_signal,
9
+ has_warning_signal,
10
+ is_biome_diagnostic_line,
11
+ is_biome_summary_line,
12
+ is_c_family_diagnostic_line,
13
+ is_c_family_source_context_line,
14
+ is_eslint_file_header,
15
+ is_eslint_issue_line,
16
+ is_eslint_summary_line,
17
+ is_golangci_lint_issue_line,
18
+ is_golangci_lint_runner_error_line,
19
+ is_golangci_lint_summary_line,
20
+ is_go_build_diagnostic_line,
21
+ is_go_build_package_header,
22
+ is_final_summary_line,
23
+ is_make_ninja_cmake_diagnostic_line,
24
+ is_markdownlint_diagnostic_line,
25
+ is_maven_gradle_diagnostic_line,
26
+ is_msbuild_diagnostic_line,
27
+ is_msbuild_summary_line,
28
+ is_mypy_diagnostic_line,
29
+ is_nx_turbo_diagnostic_line,
30
+ is_oxlint_diagnostic_line,
31
+ is_oxlint_frame_line,
32
+ is_oxlint_summary_line,
33
+ is_pyright_diagnostic_line,
34
+ is_pyright_summary_line,
35
+ is_progress_line,
36
+ is_ruff_diagnostic_line,
37
+ is_ruff_summary_line,
38
+ is_rust_diagnostic_line,
39
+ is_rust_source_bar_line,
40
+ is_rust_span_line,
41
+ is_shellcheck_diagnostic_line,
42
+ is_shellcheck_gcc_diagnostic_line,
43
+ is_shellcheck_header_line,
44
+ is_swift_xcode_failure_line,
45
+ is_tsc_diagnostic_line,
46
+ is_yamllint_diagnostic_line,
47
+ is_yamllint_file_header,
48
+ strip_ansi,
49
+ )
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class BuildCompilerDiagnostics:
54
+ """Detected diagnostic metadata."""
55
+
56
+ kind: str
57
+ failed: bool
58
+ warning: bool
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class _DiagnosticCandidate:
63
+ kind: str
64
+ score: int
65
+
66
+
67
+ def detect_build_compiler_diagnostics(
68
+ lines: list[str],
69
+ ) -> BuildCompilerDiagnostics | None:
70
+ """Return a strong compiler/static-analysis signal, or ``None``."""
71
+
72
+ clean_lines = [strip_ansi(line).rstrip() for line in lines]
73
+ nonblank = [line for line in clean_lines if line.strip()]
74
+ if len(nonblank) < 2:
75
+ return None
76
+
77
+ text = "\n".join(nonblank)
78
+ lower = text.lower()
79
+ if _looks_like_excluded_output(lower):
80
+ return None
81
+
82
+ candidates = [
83
+ candidate
84
+ for candidate in (
85
+ _classify_rust(nonblank, lower),
86
+ _classify_tsc(nonblank),
87
+ _classify_eslint(nonblank),
88
+ _classify_mypy(nonblank),
89
+ _classify_ruff(nonblank),
90
+ _classify_pyright(nonblank),
91
+ _classify_biome(nonblank),
92
+ _classify_oxlint(nonblank),
93
+ _classify_shellcheck(nonblank),
94
+ _classify_markdownlint(nonblank, lower),
95
+ _classify_yamllint(nonblank, lower),
96
+ _classify_golangci_lint(nonblank, lower),
97
+ _classify_c_family(nonblank, lower),
98
+ _classify_go_build(nonblank),
99
+ _classify_msbuild_dotnet(nonblank, lower),
100
+ _classify_maven_gradle(nonblank, lower),
101
+ _classify_make_ninja_cmake(nonblank),
102
+ _classify_nx_turbo(nonblank, lower),
103
+ _classify_swift_xcode(nonblank, lower),
104
+ )
105
+ if candidate is not None
106
+ ]
107
+ if not candidates:
108
+ return None
109
+
110
+ kind = max(
111
+ candidates,
112
+ key=lambda candidate: (candidate.score, candidate.kind),
113
+ ).kind
114
+ failed = has_error_signal(nonblank) or _kind_defaults_to_failure(kind, nonblank)
115
+ warning = has_warning_signal(nonblank) or _kind_defaults_to_warning(kind, nonblank)
116
+ return BuildCompilerDiagnostics(
117
+ kind=kind,
118
+ failed=failed,
119
+ warning=warning,
120
+ )
121
+
122
+
123
+ def _candidate(kind: str, enabled: bool, score: int) -> _DiagnosticCandidate | None:
124
+ if not enabled:
125
+ return None
126
+ return _DiagnosticCandidate(kind=kind, score=score)
127
+
128
+
129
+ def _classify_rust(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
130
+ rust_headers = sum(1 for line in lines if is_rust_diagnostic_line(line))
131
+ rust_spans = sum(1 for line in lines if is_rust_span_line(line))
132
+ rust_bars = sum(1 for line in lines if is_rust_source_bar_line(line))
133
+ progress_lines = sum(1 for line in lines if is_progress_line(line))
134
+ signal = rust_headers > 0 and (
135
+ rust_spans > 0
136
+ or rust_bars > 0
137
+ or "could not compile" in lower
138
+ or "aborting due to" in lower
139
+ )
140
+ return _candidate(
141
+ "cargo/rustc",
142
+ signal,
143
+ rust_headers * 3 + rust_spans * 2 + rust_bars + progress_lines,
144
+ )
145
+
146
+
147
+ def _classify_tsc(lines: list[str]) -> _DiagnosticCandidate | None:
148
+ tsc_lines = sum(1 for line in lines if is_tsc_diagnostic_line(line))
149
+ return _candidate("tsc", tsc_lines > 0, tsc_lines * 4)
150
+
151
+
152
+ def _classify_eslint(lines: list[str]) -> _DiagnosticCandidate | None:
153
+ eslint_issues = sum(1 for line in lines if is_eslint_issue_line(line))
154
+ eslint_headers = sum(1 for line in lines if is_eslint_file_header(line))
155
+ eslint_summaries = sum(1 for line in lines if is_eslint_summary_line(line))
156
+ return _candidate(
157
+ "eslint",
158
+ eslint_issues > 0 and (eslint_headers > 0 or eslint_summaries > 0),
159
+ eslint_issues * 3 + eslint_headers + eslint_summaries * 2,
160
+ )
161
+
162
+
163
+ def _classify_mypy(lines: list[str]) -> _DiagnosticCandidate | None:
164
+ mypy_lines = sum(1 for line in lines if is_mypy_diagnostic_line(line))
165
+ mypy_summaries = sum(1 for line in lines if _is_mypy_summary_or_code_line(line))
166
+ return _candidate(
167
+ "mypy",
168
+ mypy_lines > 0 and (mypy_summaries > 0 or mypy_lines >= 3),
169
+ mypy_lines * 3 + mypy_summaries * 2,
170
+ )
171
+
172
+
173
+ def _classify_ruff(lines: list[str]) -> _DiagnosticCandidate | None:
174
+ ruff_lines = sum(1 for line in lines if is_ruff_diagnostic_line(line))
175
+ ruff_summaries = sum(1 for line in lines if is_ruff_summary_line(line))
176
+ return _candidate(
177
+ "ruff",
178
+ ruff_lines > 0 and (ruff_summaries > 0 or ruff_lines >= 2),
179
+ ruff_lines * 4 + ruff_summaries * 2,
180
+ )
181
+
182
+
183
+ def _classify_pyright(lines: list[str]) -> _DiagnosticCandidate | None:
184
+ pyright_lines = sum(1 for line in lines if is_pyright_diagnostic_line(line))
185
+ pyright_summaries = sum(1 for line in lines if is_pyright_summary_line(line))
186
+ return _candidate(
187
+ "pyright",
188
+ pyright_lines > 0 and (pyright_summaries > 0 or pyright_lines >= 2),
189
+ pyright_lines * 4 + pyright_summaries * 2,
190
+ )
191
+
192
+
193
+ def _classify_biome(lines: list[str]) -> _DiagnosticCandidate | None:
194
+ biome_lines = sum(1 for line in lines if is_biome_diagnostic_line(line))
195
+ biome_summaries = sum(1 for line in lines if is_biome_summary_line(line))
196
+ return _candidate(
197
+ "biome",
198
+ biome_lines > 0 and (biome_summaries > 0 or biome_lines >= 2),
199
+ biome_lines * 3 + biome_summaries * 2,
200
+ )
201
+
202
+
203
+ def _classify_oxlint(lines: list[str]) -> _DiagnosticCandidate | None:
204
+ oxlint_lines = sum(1 for line in lines if is_oxlint_diagnostic_line(line))
205
+ oxlint_frames = sum(1 for line in lines if is_oxlint_frame_line(line))
206
+ oxlint_summaries = sum(1 for line in lines if is_oxlint_summary_line(line))
207
+ return _candidate(
208
+ "oxlint",
209
+ oxlint_lines > 0
210
+ and (oxlint_summaries > 0 or oxlint_frames > 0 or oxlint_lines >= 2),
211
+ oxlint_lines * 4 + oxlint_frames * 2 + oxlint_summaries * 2,
212
+ )
213
+
214
+
215
+ def _classify_shellcheck(lines: list[str]) -> _DiagnosticCandidate | None:
216
+ shellcheck_headers = sum(1 for line in lines if is_shellcheck_header_line(line))
217
+ shellcheck_lines = sum(1 for line in lines if is_shellcheck_diagnostic_line(line))
218
+ shellcheck_gcc_lines = sum(
219
+ 1 for line in lines if is_shellcheck_gcc_diagnostic_line(line)
220
+ )
221
+ return _candidate(
222
+ "shellcheck",
223
+ shellcheck_lines > 0
224
+ and (
225
+ shellcheck_headers > 0
226
+ or shellcheck_gcc_lines > 0
227
+ or shellcheck_lines >= 2
228
+ ),
229
+ shellcheck_lines * 4 + shellcheck_headers * 2,
230
+ )
231
+
232
+
233
+ def _classify_markdownlint(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
234
+ markdownlint_lines = sum(
235
+ 1 for line in lines if is_markdownlint_diagnostic_line(line)
236
+ )
237
+ return _candidate(
238
+ "markdownlint",
239
+ markdownlint_lines > 0
240
+ and (
241
+ markdownlint_lines >= 2
242
+ or "markdownlint" in lower
243
+ or any(is_final_summary_line(line) for line in lines)
244
+ ),
245
+ markdownlint_lines * 4,
246
+ )
247
+
248
+
249
+ def _classify_yamllint(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
250
+ yamllint_lines = sum(1 for line in lines if is_yamllint_diagnostic_line(line))
251
+ yamllint_headers = sum(1 for line in lines if is_yamllint_file_header(line))
252
+ return _candidate(
253
+ "yamllint",
254
+ yamllint_lines > 0
255
+ and (yamllint_headers > 0 or yamllint_lines >= 2 or "yamllint" in lower),
256
+ yamllint_lines * 4 + yamllint_headers,
257
+ )
258
+
259
+
260
+ def _classify_golangci_lint(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
261
+ golangci_lines = sum(1 for line in lines if is_golangci_lint_issue_line(line))
262
+ golangci_runner_errors = sum(
263
+ 1 for line in lines if is_golangci_lint_runner_error_line(line)
264
+ )
265
+ golangci_summaries = sum(
266
+ 1 for line in lines if is_golangci_lint_summary_line(line)
267
+ )
268
+ progress_lines = sum(1 for line in lines if is_progress_line(line))
269
+ signal = (
270
+ golangci_lines > 0
271
+ and (
272
+ golangci_lines >= 2
273
+ or golangci_summaries > 0
274
+ or golangci_runner_errors > 0
275
+ or "golangci-lint" in lower
276
+ )
277
+ ) or (
278
+ golangci_runner_errors > 0
279
+ and ("golangci-lint" in lower or "level=info" in lower or progress_lines > 0)
280
+ )
281
+ return _candidate(
282
+ "golangci-lint",
283
+ signal,
284
+ golangci_lines * 4 + golangci_runner_errors * 3 + golangci_summaries,
285
+ )
286
+
287
+
288
+ def _classify_c_family(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
289
+ diagnostics = sum(1 for line in lines if is_c_family_diagnostic_line(line))
290
+ context = sum(1 for line in lines if is_c_family_source_context_line(line))
291
+ summary = int("build failed" in lower or "compilation terminated" in lower)
292
+ signal = diagnostics > 0 and (diagnostics >= 2 or context > 0 or summary > 0)
293
+ return _candidate(
294
+ "gcc/clang",
295
+ signal,
296
+ diagnostics * 4 + context + summary * 2,
297
+ )
298
+
299
+
300
+ def _classify_go_build(lines: list[str]) -> _DiagnosticCandidate | None:
301
+ headers = sum(1 for line in lines if is_go_build_package_header(line))
302
+ diagnostics = sum(1 for line in lines if is_go_build_diagnostic_line(line))
303
+ return _candidate(
304
+ "go-build",
305
+ headers > 0 and diagnostics > 0,
306
+ headers * 2 + diagnostics * 4,
307
+ )
308
+
309
+
310
+ def _classify_msbuild_dotnet(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
311
+ diagnostics = sum(1 for line in lines if is_msbuild_diagnostic_line(line))
312
+ summaries = sum(1 for line in lines if is_msbuild_summary_line(line))
313
+ marker = "build failed" in lower or "msbuild" in lower or "dotnet build" in lower
314
+ return _candidate(
315
+ "msbuild/dotnet",
316
+ diagnostics > 0 and (summaries > 0 or marker),
317
+ diagnostics * 4 + summaries * 2 + int(marker),
318
+ )
319
+
320
+
321
+ def _classify_maven_gradle(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
322
+ diagnostics = sum(1 for line in lines if is_maven_gradle_diagnostic_line(line))
323
+ marker = any(
324
+ token in lower
325
+ for token in (
326
+ "maven-surefire-plugin",
327
+ "[error] build failure",
328
+ "build failure",
329
+ "gradle",
330
+ "> task ",
331
+ "execution failed for task",
332
+ )
333
+ )
334
+ return _candidate(
335
+ "maven/gradle",
336
+ diagnostics > 0 and marker,
337
+ diagnostics * 3 + int(marker) * 2,
338
+ )
339
+
340
+
341
+ def _classify_make_ninja_cmake(lines: list[str]) -> _DiagnosticCandidate | None:
342
+ diagnostics = sum(1 for line in lines if is_make_ninja_cmake_diagnostic_line(line))
343
+ return _candidate(
344
+ "make/ninja/cmake",
345
+ diagnostics > 0,
346
+ diagnostics * 4,
347
+ )
348
+
349
+
350
+ def _classify_nx_turbo(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
351
+ diagnostics = sum(1 for line in lines if is_nx_turbo_diagnostic_line(line))
352
+ marker = "nx " in lower or "nx " in lower or "turbo" in lower
353
+ return _candidate(
354
+ "nx/turbo",
355
+ diagnostics > 0 and marker,
356
+ diagnostics * 3 + int(marker) * 2,
357
+ )
358
+
359
+
360
+ def _classify_swift_xcode(lines: list[str], lower: str) -> _DiagnosticCandidate | None:
361
+ swift_diagnostics = sum(
362
+ 1 for line in lines if is_c_family_diagnostic_line(line) and ".swift:" in line
363
+ )
364
+ xcode_failures = sum(1 for line in lines if is_swift_xcode_failure_line(line))
365
+ marker = "xcodebuild" in lower or "swiftcompile" in lower or "** build failed **" in lower
366
+ return _candidate(
367
+ "swift/xcode",
368
+ (swift_diagnostics > 0 and marker) or xcode_failures > 0,
369
+ swift_diagnostics * 4 + xcode_failures * 3 + int(marker),
370
+ )
371
+
372
+
373
+ def _looks_like_excluded_output(lower: str) -> bool:
374
+ return (
375
+ lower.startswith("test output:")
376
+ or "test session starts" in lower
377
+ or "short test summary info" in lower
378
+ or "test result:" in lower
379
+ or "traceback (most recent call last)" in lower
380
+ )
381
+
382
+
383
+ def _is_mypy_summary_or_code_line(line: str) -> bool:
384
+ lower = line.lower()
385
+ return (
386
+ ("found " in lower and " error" in lower and " file" in lower)
387
+ or "success: no issues found" in lower
388
+ or ("[" in line and "]" in line and is_mypy_diagnostic_line(line))
389
+ or is_final_summary_line(line)
390
+ )
391
+
392
+
393
+ def _kind_defaults_to_failure(kind: str, lines: list[str]) -> bool:
394
+ """Classify linter issue-only formats without treating warnings as failures."""
395
+
396
+ if kind in {"markdownlint", "golangci-lint"}:
397
+ return True
398
+ if kind == "go-build":
399
+ return any(is_go_build_diagnostic_line(line) for line in lines)
400
+ if kind == "ruff":
401
+ return any(is_ruff_diagnostic_line(line) for line in lines)
402
+ if kind in {"biome", "oxlint"} and not has_warning_signal(lines):
403
+ return True
404
+ return False
405
+
406
+
407
+ def _kind_defaults_to_warning(kind: str, lines: list[str]) -> bool:
408
+ if kind in {"shellcheck", "yamllint"}:
409
+ return True
410
+ if kind in {"biome", "oxlint"} and has_warning_signal(lines):
411
+ return True
412
+ return False
@@ -0,0 +1,166 @@
1
+ """Conservative reducer for build/compiler diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..text import join_preserving_final_newline, score, split_preserving_final_newline
6
+ from .detector import BuildCompilerDiagnostics, detect_build_compiler_diagnostics
7
+ from .summary import (
8
+ diagnostic_context_budget,
9
+ is_eslint_issue_line,
10
+ is_diagnostic_file_header,
11
+ is_final_summary_line,
12
+ is_important_diagnostic_line,
13
+ is_mypy_diagnostic_line,
14
+ is_progress_line,
15
+ is_safe_snippet_line,
16
+ normalize_line,
17
+ parse_golangci_lint_issue_line,
18
+ parse_mypy_diagnostic_line,
19
+ strip_ansi,
20
+ )
21
+
22
+
23
+ def compress_build_compiler_diagnostics_output(text: str) -> str:
24
+ """Compress compiler/build/static-analysis diagnostic output."""
25
+
26
+ lines, final_newline = split_preserving_final_newline(text)
27
+ signal = detect_build_compiler_diagnostics(lines)
28
+ if signal is None:
29
+ return text
30
+
31
+ if signal.kind == "mypy":
32
+ candidate_lines = _reduce_mypy_lines(lines, signal)
33
+ else:
34
+ candidate_lines = _reduce_lines(lines, signal)
35
+ if len(candidate_lines) < 2:
36
+ return text
37
+
38
+ candidate = join_preserving_final_newline(candidate_lines, final_newline)
39
+ if score(candidate) < score(text):
40
+ return candidate
41
+ return text
42
+
43
+
44
+ def _reduce_lines(
45
+ lines: list[str], signal: BuildCompilerDiagnostics
46
+ ) -> list[str]:
47
+ selected = [_header(signal)]
48
+ progress_omitted = 0
49
+ snippet_remaining = 0
50
+
51
+ def flush_progress() -> None:
52
+ nonlocal progress_omitted
53
+ if progress_omitted:
54
+ plural = "" if progress_omitted == 1 else "s"
55
+ selected.append(f"… {progress_omitted} build progress line{plural} omitted")
56
+ progress_omitted = 0
57
+
58
+ for raw_line in lines:
59
+ line = strip_ansi(raw_line).rstrip()
60
+ stripped = line.strip()
61
+ if not stripped:
62
+ continue
63
+
64
+ if is_progress_line(stripped):
65
+ progress_omitted += 1
66
+ continue
67
+
68
+ if is_important_diagnostic_line(stripped) or is_diagnostic_file_header(
69
+ signal.kind, stripped
70
+ ):
71
+ flush_progress()
72
+ selected.append(_normalize_for_signal(line, signal))
73
+ snippet_remaining = diagnostic_context_budget(signal.kind, stripped)
74
+ continue
75
+
76
+ if snippet_remaining and is_safe_snippet_line(line):
77
+ flush_progress()
78
+ selected.append(line.rstrip())
79
+ snippet_remaining -= 1
80
+ continue
81
+
82
+ if signal.failed and is_final_summary_line(stripped):
83
+ flush_progress()
84
+ selected.append(_normalize_for_signal(line, signal))
85
+ continue
86
+
87
+ progress_omitted += 1
88
+
89
+ flush_progress()
90
+ return _drop_adjacent_duplicates(selected)
91
+
92
+
93
+ def _reduce_mypy_lines(
94
+ lines: list[str], signal: BuildCompilerDiagnostics
95
+ ) -> list[str]:
96
+ selected = [_header(signal)]
97
+ progress_omitted = 0
98
+ current_path: str | None = None
99
+
100
+ def flush_progress() -> None:
101
+ nonlocal progress_omitted
102
+ if progress_omitted:
103
+ plural = "" if progress_omitted == 1 else "s"
104
+ selected.append(f"… {progress_omitted} build progress line{plural} omitted")
105
+ progress_omitted = 0
106
+
107
+ for raw_line in lines:
108
+ line = strip_ansi(raw_line).rstrip()
109
+ stripped = line.strip()
110
+ if not stripped:
111
+ continue
112
+
113
+ parsed = parse_mypy_diagnostic_line(stripped)
114
+ if parsed is not None:
115
+ path, location, detail = parsed
116
+ flush_progress()
117
+ if path != current_path:
118
+ selected.append(path)
119
+ current_path = path
120
+ selected.append(f" {location}: {detail}")
121
+ continue
122
+
123
+ if is_progress_line(stripped):
124
+ progress_omitted += 1
125
+ continue
126
+
127
+ if is_final_summary_line(stripped) or (
128
+ is_mypy_diagnostic_line(stripped) and "[" in stripped and "]" in stripped
129
+ ):
130
+ flush_progress()
131
+ selected.append(normalize_line(line))
132
+ continue
133
+
134
+ progress_omitted += 1
135
+
136
+ flush_progress()
137
+ return _drop_adjacent_duplicates(selected)
138
+
139
+
140
+ def _header(signal: BuildCompilerDiagnostics) -> str:
141
+ outcome = "failure" if signal.failed else "warning" if signal.warning else "success"
142
+ return f"build diagnostics: {signal.kind} {outcome}"
143
+
144
+
145
+ def _normalize_for_signal(line: str, signal: BuildCompilerDiagnostics) -> str:
146
+ if signal.kind == "eslint" and is_eslint_issue_line(line):
147
+ return " ".join(line.strip().split())
148
+ if signal.kind == "golangci-lint":
149
+ parsed = parse_golangci_lint_issue_line(line)
150
+ if parsed is not None:
151
+ path, location, detail = parsed
152
+ return f"{path} {location}: {detail}"
153
+ if signal.kind == "shellcheck":
154
+ return line.rstrip()
155
+ return normalize_line(line)
156
+
157
+
158
+ def _drop_adjacent_duplicates(lines: list[str]) -> list[str]:
159
+ output: list[str] = []
160
+ for line in lines:
161
+ if not line:
162
+ continue
163
+ if output and output[-1] == line:
164
+ continue
165
+ output.append(line)
166
+ return output