codetool-shell 0.1.1__py3-none-win_arm64.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/windows-arm64/codetool-shell-rust.exe +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,99 @@
1
+ """Conservative reducer for CI/job log output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..text import join_preserving_final_newline, score, split_preserving_final_newline
6
+ from .detector import CiLogSignal, detect_ci_job_log
7
+ from .summary import (
8
+ extract_group_step,
9
+ is_group_end,
10
+ is_important_line,
11
+ normalize_selected_line,
12
+ parse_ci_line,
13
+ step_label_for_line,
14
+ )
15
+
16
+
17
+ def compress_ci_job_log_output(text: str) -> str:
18
+ """Compress GitHub Actions / gh-run style job logs."""
19
+
20
+ lines, final_newline = split_preserving_final_newline(text)
21
+ signal = detect_ci_job_log(lines)
22
+ if signal is None:
23
+ return text
24
+
25
+ candidate_lines = _reduce_lines(lines, signal)
26
+ if len(candidate_lines) < 2:
27
+ return text
28
+
29
+ candidate = join_preserving_final_newline(candidate_lines, final_newline)
30
+ if score(candidate) < score(text):
31
+ return candidate
32
+ return text
33
+
34
+
35
+ def _reduce_lines(lines: list[str], signal: CiLogSignal) -> list[str]:
36
+ outcome = "failure" if signal.failed else "warning" if signal.warning else "success"
37
+ selected = [f"ci log: github-actions {outcome}"]
38
+ current_step: str | None = None
39
+ in_group = False
40
+ omitted = 0
41
+
42
+ def flush_omitted() -> None:
43
+ nonlocal omitted
44
+ if omitted:
45
+ plural = "" if omitted == 1 else "s"
46
+ selected.append(f"… {omitted} noisy line{plural} omitted")
47
+ omitted = 0
48
+
49
+ def start_step(step: str) -> None:
50
+ nonlocal current_step
51
+ clean_step = step.strip()
52
+ if not clean_step or clean_step == current_step:
53
+ return
54
+ flush_omitted()
55
+ selected.append(f"step: {clean_step}")
56
+ current_step = clean_step
57
+
58
+ for raw_line in lines:
59
+ parsed = parse_ci_line(raw_line)
60
+ message = parsed.message.strip()
61
+ if not message:
62
+ continue
63
+
64
+ group_step = extract_group_step(message)
65
+ if group_step is not None:
66
+ start_step(group_step)
67
+ in_group = True
68
+ continue
69
+
70
+ prefixed_step = step_label_for_line(parsed)
71
+ if prefixed_step is not None and not in_group:
72
+ start_step(prefixed_step)
73
+ if prefixed_step == message:
74
+ continue
75
+
76
+ if is_group_end(message):
77
+ in_group = False
78
+ continue
79
+
80
+ if is_important_line(message):
81
+ flush_omitted()
82
+ selected.append(normalize_selected_line(message))
83
+ continue
84
+
85
+ omitted += 1
86
+
87
+ flush_omitted()
88
+ return _drop_blank_and_adjacent_duplicates(selected)
89
+
90
+
91
+ def _drop_blank_and_adjacent_duplicates(lines: list[str]) -> list[str]:
92
+ output: list[str] = []
93
+ for line in lines:
94
+ if not line:
95
+ continue
96
+ if output and output[-1] == line:
97
+ continue
98
+ output.append(line)
99
+ return output
@@ -0,0 +1,243 @@
1
+ """Line parsing and classification for CI/job logs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CiLine:
11
+ """A parsed CI log line with noisy runner prefix removed."""
12
+
13
+ message: str
14
+ job: str | None
15
+ step: str | None
16
+ had_ci_prefix: bool
17
+
18
+
19
+ def parse_ci_line(line: str) -> CiLine:
20
+ """Return CI prefix metadata and the visible log message."""
21
+
22
+ tab_parts = line.split("\t", 3)
23
+ if len(tab_parts) == 4 and _is_github_timestamp(tab_parts[2].strip()):
24
+ return CiLine(
25
+ message=tab_parts[3].strip(),
26
+ job=tab_parts[0].strip() or None,
27
+ step=tab_parts[1].strip() or None,
28
+ had_ci_prefix=True,
29
+ )
30
+ if len(tab_parts) == 3:
31
+ timestamp, separator, rest = tab_parts[2].strip().partition(" ")
32
+ if separator and _is_github_timestamp(timestamp):
33
+ return CiLine(
34
+ message=rest.strip(),
35
+ job=tab_parts[0].strip() or None,
36
+ step=tab_parts[1].strip() or None,
37
+ had_ci_prefix=True,
38
+ )
39
+
40
+ stripped = line.lstrip()
41
+ first, separator, rest = stripped.partition(" ")
42
+ if separator and _is_github_timestamp(first):
43
+ return CiLine(
44
+ message=rest.strip(),
45
+ job=None,
46
+ step=None,
47
+ had_ci_prefix=True,
48
+ )
49
+
50
+ return CiLine(message=line.strip(), job=None, step=None, had_ci_prefix=False)
51
+
52
+
53
+ def extract_group_step(message: str) -> str | None:
54
+ """Return a GitHub Actions group title if ``message`` opens a group."""
55
+
56
+ stripped = message.strip()
57
+ for prefix in ("##[group]", "::group::"):
58
+ if stripped.startswith(prefix):
59
+ title = stripped[len(prefix) :].strip()
60
+ return title or None
61
+ return None
62
+
63
+
64
+ def is_group_end(message: str) -> bool:
65
+ """Return true for a GitHub Actions group terminator."""
66
+
67
+ stripped = message.strip()
68
+ return stripped.startswith(("##[endgroup]", "::endgroup::"))
69
+
70
+
71
+ def step_label_for_line(parsed: CiLine) -> str | None:
72
+ """Return a compact step label from parsed prefix data or a run marker."""
73
+
74
+ if parsed.step:
75
+ if parsed.job and parsed.job != parsed.step:
76
+ return f"{parsed.job} / {parsed.step}"
77
+ return parsed.step
78
+
79
+ message = parsed.message.strip()
80
+ if _is_step_start_message(message):
81
+ return message
82
+ return None
83
+
84
+
85
+ def is_actions_marker(message: str) -> bool:
86
+ """Return true for GitHub Actions control/annotation markers."""
87
+
88
+ stripped = message.strip()
89
+ return stripped.startswith("##[") or stripped.startswith(
90
+ (
91
+ "::group::",
92
+ "::endgroup::",
93
+ "::error",
94
+ "::warning",
95
+ "::notice",
96
+ "::debug",
97
+ )
98
+ )
99
+
100
+
101
+ def is_error_annotation(message: str) -> bool:
102
+ """Return true for GitHub Actions error annotations."""
103
+
104
+ stripped = message.strip()
105
+ return stripped.startswith(("::error", "##[error]"))
106
+
107
+
108
+ def is_warning_annotation(message: str) -> bool:
109
+ """Return true for GitHub Actions warning annotations."""
110
+
111
+ stripped = message.strip()
112
+ return stripped.startswith(("::warning", "##[warning]"))
113
+
114
+
115
+ def is_warning_line(message: str) -> bool:
116
+ """Return true for warning lines worth preserving."""
117
+
118
+ stripped = message.strip()
119
+ return is_warning_annotation(stripped) or stripped.startswith(("Warning:", "WARNING:"))
120
+
121
+
122
+ def is_failure_line(message: str) -> bool:
123
+ """Return true for failure/exit lines worth preserving."""
124
+
125
+ stripped = message.strip()
126
+ lower = stripped.lower()
127
+ return (
128
+ is_error_annotation(stripped)
129
+ or stripped.startswith(("Error:", "ERROR:", "Failed", "FAILED", "fatal:"))
130
+ or "process completed with exit code" in lower
131
+ or "failed with exit code" in lower
132
+ or "exit code 1" in lower
133
+ or "status: failure" in lower
134
+ or "result: failure" in lower
135
+ or "conclusion: failure" in lower
136
+ )
137
+
138
+
139
+ def is_conclusion_line(message: str) -> bool:
140
+ """Return true for final job/workflow status lines."""
141
+
142
+ lower = message.strip().lower()
143
+ return (
144
+ "workflow completed" in lower
145
+ or "job completed" in lower
146
+ or "completed with status" in lower
147
+ or "completed with result" in lower
148
+ or "completed successfully" in lower
149
+ or "conclusion:" in lower
150
+ )
151
+
152
+
153
+ def is_reference_line(message: str) -> bool:
154
+ """Return true for URLs and artifact/path references."""
155
+
156
+ stripped = message.strip()
157
+ lower = stripped.lower()
158
+ if "https://" in lower or "http://" in lower:
159
+ return True
160
+ if "artifact" in lower:
161
+ return True
162
+ if any(token in lower for token in _REFERENCE_TOKENS):
163
+ return True
164
+ return _looks_like_path_reference(stripped)
165
+
166
+
167
+ def is_path_location_line(message: str) -> bool:
168
+ """Return true for source file locations like ``src/app.ts:12:3``."""
169
+
170
+ stripped = message.strip().removeprefix("at ").strip()
171
+ parts = stripped.split(":")
172
+ if len(parts) < 2:
173
+ return False
174
+ for index in range(1, len(parts)):
175
+ if parts[index].isdigit():
176
+ path = ":".join(parts[:index])
177
+ return _looks_like_path(path)
178
+ return False
179
+
180
+
181
+ def is_important_line(message: str) -> bool:
182
+ """Return true when a CI log line must be preserved."""
183
+
184
+ stripped = message.strip()
185
+ return (
186
+ is_warning_line(stripped)
187
+ or is_failure_line(stripped)
188
+ or is_conclusion_line(stripped)
189
+ or is_reference_line(stripped)
190
+ or is_path_location_line(stripped)
191
+ )
192
+
193
+
194
+ def normalize_selected_line(message: str) -> str:
195
+ """Normalize a selected CI log message."""
196
+
197
+ return message.strip()
198
+
199
+
200
+ def _is_step_start_message(message: str) -> bool:
201
+ return message.startswith(("Run ", "Post ", "Complete job", "Set up job", "Prepare "))
202
+
203
+
204
+ def _is_github_timestamp(value: str) -> bool:
205
+ return _GITHUB_TIMESTAMP_RE.match(value) is not None
206
+
207
+
208
+ def _looks_like_path_reference(line: str) -> bool:
209
+ lower = line.lower()
210
+ if not any(suffix in lower for suffix in _REFERENCE_SUFFIXES):
211
+ return False
212
+ return "/" in line or "\\" in line
213
+
214
+
215
+ def _looks_like_path(path: str) -> bool:
216
+ if not path or path[0].isspace() or "://" in path:
217
+ return False
218
+ return "/" in path or "\\" in path or "." in path
219
+
220
+
221
+ _GITHUB_TIMESTAMP_RE = re.compile(
222
+ r"^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$"
223
+ )
224
+ _REFERENCE_TOKENS = (
225
+ "test-results/",
226
+ "playwright-report/",
227
+ "coverage/",
228
+ "dist/",
229
+ "build/",
230
+ "logs/",
231
+ )
232
+ _REFERENCE_SUFFIXES = (
233
+ ".zip",
234
+ ".tar.gz",
235
+ ".log",
236
+ ".txt",
237
+ ".html",
238
+ ".xml",
239
+ ".json",
240
+ ".png",
241
+ ".jpg",
242
+ ".jpeg",
243
+ )
@@ -0,0 +1,7 @@
1
+ """Generic unified diff and diff-noise compression."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .reducer import compress_diff_output
6
+
7
+ __all__ = ["compress_diff_output"]
@@ -0,0 +1,136 @@
1
+ """Conservative unified-diff block detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class DiffBlock:
11
+ """Line range for one high-confidence diff block."""
12
+
13
+ start: int
14
+ end: int
15
+
16
+
17
+ _NORMAL_HUNK_RE = re.compile(
18
+ r"^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@(?: .*)?$"
19
+ )
20
+ _COMBINED_HUNK_RE = re.compile(
21
+ r"^(?P<ats>@{3,}) (?:-\d+(?:,\d+)? )+\+\d+(?:,\d+)? (?P=ats)(?: .*)?$"
22
+ )
23
+ _DIFF_HEADER_PREFIXES = ("diff --git ", "diff --cc ", "diff --combined ")
24
+ _DIFF_METADATA_PREFIXES = (
25
+ "new file mode ",
26
+ "deleted file mode ",
27
+ "old mode ",
28
+ "new mode ",
29
+ "similarity index ",
30
+ "dissimilarity index ",
31
+ "rename from ",
32
+ "rename to ",
33
+ "copy from ",
34
+ "copy to ",
35
+ )
36
+
37
+
38
+ def find_diff_blocks(lines: list[str]) -> tuple[DiffBlock, ...]:
39
+ """Return high-confidence diff block ranges."""
40
+
41
+ blocks: list[DiffBlock] = []
42
+ i = 0
43
+ while i < len(lines):
44
+ kind = _block_start_kind(lines, i)
45
+ if kind is None:
46
+ i += 1
47
+ continue
48
+
49
+ end = _find_block_end(lines, i, kind)
50
+ block = lines[i:end]
51
+ if _is_valid_diff_block(block, kind):
52
+ blocks.append(DiffBlock(i, end))
53
+ i = end
54
+ else:
55
+ i += 1
56
+
57
+ return tuple(blocks)
58
+
59
+
60
+ def is_hunk_header(line: str) -> bool:
61
+ """Return whether ``line`` is a normal or combined hunk header."""
62
+
63
+ return _NORMAL_HUNK_RE.match(line) is not None or _COMBINED_HUNK_RE.match(line) is not None
64
+
65
+
66
+ def hunk_prefix_columns(header: str) -> int:
67
+ """Return body prefix column count for a hunk header."""
68
+
69
+ if header.startswith("@@@"):
70
+ at_count = len(header) - len(header.lstrip("@"))
71
+ return max(2, at_count - 1)
72
+ return 1
73
+
74
+
75
+ def _block_start_kind(lines: list[str], index: int) -> str | None:
76
+ line = lines[index]
77
+ if line.startswith(_DIFF_HEADER_PREFIXES):
78
+ return "diff"
79
+ if line.startswith("Index: "):
80
+ return "svn"
81
+ if _has_unified_file_header_pair_at(lines, index):
82
+ return "standalone"
83
+ return None
84
+
85
+
86
+ def _find_block_end(lines: list[str], start: int, kind: str) -> int:
87
+ i = start + 1
88
+ while i < len(lines):
89
+ if lines[i].startswith(_DIFF_HEADER_PREFIXES) or lines[i].startswith("Index: "):
90
+ return i
91
+ if kind == "diff" and _is_git_patch_preamble_line(lines[i]):
92
+ return i
93
+ if kind == "standalone" and _has_unified_file_header_pair_at(lines, i):
94
+ return i
95
+ i += 1
96
+ return len(lines)
97
+
98
+
99
+ def _has_unified_file_header_pair_at(lines: list[str], index: int) -> bool:
100
+ return (
101
+ index + 2 < len(lines)
102
+ and lines[index].startswith("--- ")
103
+ and lines[index + 1].startswith("+++ ")
104
+ and is_hunk_header(lines[index + 2])
105
+ )
106
+
107
+
108
+ def _is_valid_diff_block(block: list[str], kind: str) -> bool:
109
+ has_hunk = any(is_hunk_header(line) for line in block)
110
+ has_file_headers = any(
111
+ block[index].startswith("--- ") and block[index + 1].startswith("+++ ")
112
+ for index in range(max(0, len(block) - 1))
113
+ )
114
+ has_binary = any(
115
+ line.startswith(("GIT binary patch", "Binary files ", "literal ", "delta "))
116
+ for line in block
117
+ )
118
+ has_metadata = any(line.startswith(_DIFF_METADATA_PREFIXES) for line in block)
119
+
120
+ if kind == "diff":
121
+ return has_hunk or has_binary or has_file_headers or has_metadata
122
+ return has_hunk and has_file_headers
123
+
124
+
125
+ def _is_git_patch_preamble_line(line: str) -> bool:
126
+ if line.startswith("commit "):
127
+ token = line.removeprefix("commit ").split(maxsplit=1)[0]
128
+ return _is_hex_prefix(token)
129
+ if line.startswith("From "):
130
+ token = line.removeprefix("From ").split(maxsplit=1)[0]
131
+ return _is_hex_prefix(token)
132
+ return False
133
+
134
+
135
+ def _is_hex_prefix(token: str) -> bool:
136
+ return len(token) >= 7 and all(char in "0123456789abcdefABCDEF" for char in token)