codetool-shell 0.1.1__py3-none-win_amd64.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.
- codetool_shell/__init__.py +11 -0
- codetool_shell/api.py +59 -0
- codetool_shell/bin/windows-x86_64/codetool-shell-rust.exe +0 -0
- codetool_shell/filters/__init__.py +14 -0
- codetool_shell/filters/build_compiler/__init__.py +7 -0
- codetool_shell/filters/build_compiler/detector.py +412 -0
- codetool_shell/filters/build_compiler/reducer.py +166 -0
- codetool_shell/filters/build_compiler/summary.py +617 -0
- codetool_shell/filters/ci_job_log/__init__.py +7 -0
- codetool_shell/filters/ci_job_log/detector.py +64 -0
- codetool_shell/filters/ci_job_log/reducer.py +99 -0
- codetool_shell/filters/ci_job_log/summary.py +243 -0
- codetool_shell/filters/diff/__init__.py +7 -0
- codetool_shell/filters/diff/detector.py +136 -0
- codetool_shell/filters/diff/reducer.py +308 -0
- codetool_shell/filters/generic_log/__init__.py +7 -0
- codetool_shell/filters/generic_log/detector.py +175 -0
- codetool_shell/filters/generic_log/reducer.py +99 -0
- codetool_shell/filters/generic_log/summary.py +161 -0
- codetool_shell/filters/git.py +514 -0
- codetool_shell/filters/html_cleanup/__init__.py +7 -0
- codetool_shell/filters/html_cleanup/detector.py +136 -0
- codetool_shell/filters/html_cleanup/reducer.py +27 -0
- codetool_shell/filters/html_cleanup/summary.py +422 -0
- codetool_shell/filters/json_payload/__init__.py +7 -0
- codetool_shell/filters/json_payload/detector.py +62 -0
- codetool_shell/filters/json_payload/reducer.py +81 -0
- codetool_shell/filters/json_payload/summary.py +233 -0
- codetool_shell/filters/listing/__init__.py +7 -0
- codetool_shell/filters/listing/detector.py +294 -0
- codetool_shell/filters/listing/reducer.py +30 -0
- codetool_shell/filters/log_template/__init__.py +7 -0
- codetool_shell/filters/log_template/constants.py +76 -0
- codetool_shell/filters/log_template/detector.py +331 -0
- codetool_shell/filters/log_template/reducer.py +78 -0
- codetool_shell/filters/log_template/template.py +280 -0
- codetool_shell/filters/log_template/types.py +21 -0
- codetool_shell/filters/opaque_payload/__init__.py +7 -0
- codetool_shell/filters/opaque_payload/detector.py +563 -0
- codetool_shell/filters/opaque_payload/reducer.py +142 -0
- codetool_shell/filters/opaque_payload/summary.py +61 -0
- codetool_shell/filters/package_manager/__init__.py +7 -0
- codetool_shell/filters/package_manager/detector.py +220 -0
- codetool_shell/filters/package_manager/reducer.py +110 -0
- codetool_shell/filters/package_manager/summary.py +172 -0
- codetool_shell/filters/pipeline.py +65 -0
- codetool_shell/filters/rg.py +250 -0
- codetool_shell/filters/system_output/__init__.py +7 -0
- codetool_shell/filters/system_output/detector.py +600 -0
- codetool_shell/filters/system_output/reducer.py +331 -0
- codetool_shell/filters/system_output/summary.py +164 -0
- codetool_shell/filters/table/__init__.py +7 -0
- codetool_shell/filters/table/detector.py +244 -0
- codetool_shell/filters/table/reducer.py +57 -0
- codetool_shell/filters/table/summary.py +37 -0
- codetool_shell/filters/test_runner/__init__.py +7 -0
- codetool_shell/filters/test_runner/ansi.py +80 -0
- codetool_shell/filters/test_runner/detector.py +409 -0
- codetool_shell/filters/test_runner/reducer.py +288 -0
- codetool_shell/filters/test_runner/summary.py +449 -0
- codetool_shell/filters/text.py +38 -0
- codetool_shell/filters/traceback/__init__.py +7 -0
- codetool_shell/filters/traceback/detector.py +209 -0
- codetool_shell/filters/traceback/reducer.py +141 -0
- codetool_shell/filters/traceback/summary.py +122 -0
- codetool_shell/filters/tree.py +59 -0
- codetool_shell/py.typed +0 -0
- codetool_shell/python_backend.py +38 -0
- codetool_shell/rust_backend.py +254 -0
- codetool_shell-0.1.1.dist-info/METADATA +152 -0
- codetool_shell-0.1.1.dist-info/RECORD +72 -0
- codetool_shell-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Constants for repeated log-template compaction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_MIN_NONBLANK_LINES = 16
|
|
6
|
+
_MIN_TEMPLATE_MATCHES = 8
|
|
7
|
+
_MIN_TEMPLATED_LINES = 12
|
|
8
|
+
_MIN_TEMPLATED_PERCENT = 35
|
|
9
|
+
_MAX_CAPTURED_VALUES = 5
|
|
10
|
+
_MAX_VALUE_LEN = 48
|
|
11
|
+
_MAX_SHORT_TUPLES = 6
|
|
12
|
+
|
|
13
|
+
_KNOWN_LEVELS = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"TRACE",
|
|
16
|
+
"DEBUG",
|
|
17
|
+
"INFO",
|
|
18
|
+
"NOTICE",
|
|
19
|
+
"WARN",
|
|
20
|
+
"WARNING",
|
|
21
|
+
"ERROR",
|
|
22
|
+
"FATAL",
|
|
23
|
+
"PANIC",
|
|
24
|
+
"CRITICAL",
|
|
25
|
+
"ALERT",
|
|
26
|
+
"EMERG",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
_LOW_LEVELS = frozenset({"TRACE", "DEBUG", "INFO"})
|
|
30
|
+
_IMPORTANT_KV_KEYS = frozenset({"level", "severity", "result", "outcome"})
|
|
31
|
+
_IMPORTANT_KV_VALUES = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"warn",
|
|
34
|
+
"warning",
|
|
35
|
+
"error",
|
|
36
|
+
"fatal",
|
|
37
|
+
"panic",
|
|
38
|
+
"critical",
|
|
39
|
+
"alert",
|
|
40
|
+
"emerg",
|
|
41
|
+
"fail",
|
|
42
|
+
"failed",
|
|
43
|
+
"failure",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
_SECRET_KEY_PARTS = (
|
|
47
|
+
"password",
|
|
48
|
+
"passwd",
|
|
49
|
+
"secret",
|
|
50
|
+
"token",
|
|
51
|
+
"api_key",
|
|
52
|
+
"apikey",
|
|
53
|
+
"authorization",
|
|
54
|
+
"credential",
|
|
55
|
+
"private_key",
|
|
56
|
+
"access_key",
|
|
57
|
+
)
|
|
58
|
+
_OPERATIONAL_WORDS = frozenset(
|
|
59
|
+
{
|
|
60
|
+
"batch",
|
|
61
|
+
"chunk",
|
|
62
|
+
"completed",
|
|
63
|
+
"event",
|
|
64
|
+
"handled",
|
|
65
|
+
"item",
|
|
66
|
+
"iteration",
|
|
67
|
+
"job",
|
|
68
|
+
"message",
|
|
69
|
+
"poll",
|
|
70
|
+
"processed",
|
|
71
|
+
"record",
|
|
72
|
+
"request",
|
|
73
|
+
"shard",
|
|
74
|
+
"task",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Detection and ownership guards for repeated log-template compaction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
|
|
7
|
+
from .constants import (
|
|
8
|
+
_IMPORTANT_KV_KEYS,
|
|
9
|
+
_IMPORTANT_KV_VALUES,
|
|
10
|
+
_LOW_LEVELS,
|
|
11
|
+
_MIN_NONBLANK_LINES,
|
|
12
|
+
_MIN_TEMPLATE_MATCHES,
|
|
13
|
+
_MIN_TEMPLATED_LINES,
|
|
14
|
+
_MIN_TEMPLATED_PERCENT,
|
|
15
|
+
)
|
|
16
|
+
from .template import (
|
|
17
|
+
_is_secret_key,
|
|
18
|
+
_looks_like_timestamp,
|
|
19
|
+
_normalize_level,
|
|
20
|
+
_parse_key_value_core,
|
|
21
|
+
_parse_key_value_token,
|
|
22
|
+
_split_wrapping_punctuation,
|
|
23
|
+
template_for_line,
|
|
24
|
+
)
|
|
25
|
+
from .types import LogTemplateSignal
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect_log_templates(lines: list[str]) -> LogTemplateSignal | None:
|
|
29
|
+
if _has_other_filter_ownership(lines):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
nonblank_count = sum(1 for line in lines if line.strip())
|
|
33
|
+
if nonblank_count < _MIN_NONBLANK_LINES:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
line_templates = tuple(
|
|
37
|
+
None
|
|
38
|
+
if _is_important_line(line.strip()) or _has_forbidden_fragment(line.strip())
|
|
39
|
+
else template_for_line(line)
|
|
40
|
+
for line in lines
|
|
41
|
+
)
|
|
42
|
+
counts: Counter[str] = Counter(
|
|
43
|
+
candidate.template for candidate in line_templates if candidate is not None
|
|
44
|
+
)
|
|
45
|
+
repeated = frozenset(
|
|
46
|
+
template for template, count in counts.items() if count >= _MIN_TEMPLATE_MATCHES
|
|
47
|
+
)
|
|
48
|
+
if not repeated:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
templated_total = sum(counts[template] for template in repeated)
|
|
52
|
+
if templated_total < _MIN_TEMPLATED_LINES:
|
|
53
|
+
return None
|
|
54
|
+
if templated_total * 100 < nonblank_count * _MIN_TEMPLATED_PERCENT:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
return LogTemplateSignal(
|
|
58
|
+
line_templates=line_templates,
|
|
59
|
+
repeated_templates=repeated,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_important_line(line: str) -> bool:
|
|
64
|
+
lower = line.lower()
|
|
65
|
+
if (
|
|
66
|
+
" warn" in f" {lower}"
|
|
67
|
+
or " warning" in f" {lower}"
|
|
68
|
+
or " error" in f" {lower}"
|
|
69
|
+
or " fatal" in f" {lower}"
|
|
70
|
+
or " panic" in f" {lower}"
|
|
71
|
+
or "exception" in lower
|
|
72
|
+
or "traceback" in lower
|
|
73
|
+
or "stack trace" in lower
|
|
74
|
+
or "failed" in lower
|
|
75
|
+
or "failure" in lower
|
|
76
|
+
or "exit code" in lower
|
|
77
|
+
):
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
tokens = line.split()
|
|
81
|
+
if tokens:
|
|
82
|
+
if _line_has_important_key_value(tokens):
|
|
83
|
+
return True
|
|
84
|
+
first = tokens[0].strip("[]")
|
|
85
|
+
if _looks_like_timestamp(first) and len(tokens) > 1:
|
|
86
|
+
level = _normalize_level(tokens[1].strip("[]:=-"))
|
|
87
|
+
return level is not None and level not in _LOW_LEVELS
|
|
88
|
+
level = _normalize_level(first.strip(":=-"))
|
|
89
|
+
if level is not None:
|
|
90
|
+
return level not in _LOW_LEVELS
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def _line_has_important_key_value(tokens: list[str]) -> bool:
|
|
94
|
+
for index, token in enumerate(tokens):
|
|
95
|
+
_, core, _ = _split_wrapping_punctuation(token)
|
|
96
|
+
key_value = _parse_key_value_core(core)
|
|
97
|
+
if key_value is not None:
|
|
98
|
+
key, value = key_value
|
|
99
|
+
if _is_important_kv(key, value):
|
|
100
|
+
return True
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if ":" not in core:
|
|
104
|
+
continue
|
|
105
|
+
key, value = core.split(":", 1)
|
|
106
|
+
if value:
|
|
107
|
+
if _is_important_kv(key, value):
|
|
108
|
+
return True
|
|
109
|
+
elif index + 1 < len(tokens) and _is_important_kv(key, tokens[index + 1]):
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def _is_important_kv(key: str, value: str) -> bool:
|
|
114
|
+
normalized_key = key.lower().replace("-", "_").replace(".", "_")
|
|
115
|
+
if normalized_key not in _IMPORTANT_KV_KEYS:
|
|
116
|
+
return False
|
|
117
|
+
normalized_level = _normalize_level(value.strip("[],:;"))
|
|
118
|
+
if normalized_level is not None:
|
|
119
|
+
return normalized_level not in _LOW_LEVELS
|
|
120
|
+
return value.strip("[],:;").lower() in _IMPORTANT_KV_VALUES
|
|
121
|
+
|
|
122
|
+
def _has_forbidden_fragment(line: str) -> bool:
|
|
123
|
+
lower = line.lower()
|
|
124
|
+
return (
|
|
125
|
+
"://" in line
|
|
126
|
+
or "www." in lower
|
|
127
|
+
or _line_has_secret_like_key(line)
|
|
128
|
+
or _line_has_path(line)
|
|
129
|
+
or any(marker in lower for marker in ("file \"", " at ", "caused by:"))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _line_has_secret_like_key(line: str) -> bool:
|
|
133
|
+
tokens = line.split()
|
|
134
|
+
for index, token in enumerate(tokens):
|
|
135
|
+
_, core, _ = _split_wrapping_punctuation(token)
|
|
136
|
+
key_value = _parse_key_value_token(token)
|
|
137
|
+
if key_value is not None and _is_secret_key(key_value[0]):
|
|
138
|
+
return True
|
|
139
|
+
if ":" not in core:
|
|
140
|
+
continue
|
|
141
|
+
key, value = core.split(":", 1)
|
|
142
|
+
if _is_secret_key(key) and (value or index + 1 < len(tokens)):
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def _line_has_path(line: str) -> bool:
|
|
147
|
+
file_suffixes = (
|
|
148
|
+
".py",
|
|
149
|
+
".rs",
|
|
150
|
+
".ts",
|
|
151
|
+
".tsx",
|
|
152
|
+
".js",
|
|
153
|
+
".jsx",
|
|
154
|
+
".json",
|
|
155
|
+
".yaml",
|
|
156
|
+
".yml",
|
|
157
|
+
".toml",
|
|
158
|
+
".log",
|
|
159
|
+
".txt",
|
|
160
|
+
".html",
|
|
161
|
+
".xml",
|
|
162
|
+
".csv",
|
|
163
|
+
".md",
|
|
164
|
+
".c",
|
|
165
|
+
".cpp",
|
|
166
|
+
".go",
|
|
167
|
+
".rb",
|
|
168
|
+
".zip",
|
|
169
|
+
".png",
|
|
170
|
+
)
|
|
171
|
+
lockfile_names = (
|
|
172
|
+
"cargo.lock",
|
|
173
|
+
"package-lock.json",
|
|
174
|
+
"yarn.lock",
|
|
175
|
+
"pnpm-lock.yaml",
|
|
176
|
+
"poetry.lock",
|
|
177
|
+
"go.sum",
|
|
178
|
+
)
|
|
179
|
+
for token in line.split():
|
|
180
|
+
cleaned = token.strip("\"'(),;[]")
|
|
181
|
+
if "://" in cleaned:
|
|
182
|
+
return True
|
|
183
|
+
lower = cleaned.lower()
|
|
184
|
+
if (
|
|
185
|
+
lower in lockfile_names
|
|
186
|
+
or any(lower.endswith(f"={name}") for name in lockfile_names)
|
|
187
|
+
or any(lower.endswith(f"/{name}") for name in lockfile_names)
|
|
188
|
+
or any(lower.endswith(f"\\{name}") for name in lockfile_names)
|
|
189
|
+
or any(
|
|
190
|
+
lower.endswith(suffix) or f"{suffix}:" in lower
|
|
191
|
+
for suffix in file_suffixes
|
|
192
|
+
)
|
|
193
|
+
):
|
|
194
|
+
return True
|
|
195
|
+
if "/" not in cleaned and "\\" not in cleaned:
|
|
196
|
+
continue
|
|
197
|
+
slash_parts = [part for part in cleaned.replace("\\", "/").split("/") if part]
|
|
198
|
+
if (
|
|
199
|
+
cleaned.startswith(("/", "./", "../", "~/"))
|
|
200
|
+
or ":\\" in cleaned
|
|
201
|
+
or len(slash_parts) >= 3
|
|
202
|
+
):
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def _has_other_filter_ownership(lines: list[str]) -> bool:
|
|
207
|
+
stripped = [line.strip() for line in lines if line.strip()]
|
|
208
|
+
if not stripped:
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
lower = "\n".join(stripped).lower()
|
|
212
|
+
if any(
|
|
213
|
+
marker in lower
|
|
214
|
+
for marker in (
|
|
215
|
+
"log templates: repeated line summary",
|
|
216
|
+
"generic log:",
|
|
217
|
+
"test output:",
|
|
218
|
+
"ci log:",
|
|
219
|
+
"python traceback:",
|
|
220
|
+
"traceback (most recent call last)",
|
|
221
|
+
"short test summary info",
|
|
222
|
+
"diff --git ",
|
|
223
|
+
"build diagnostics:",
|
|
224
|
+
"package manager:",
|
|
225
|
+
"jsonl records=",
|
|
226
|
+
)
|
|
227
|
+
):
|
|
228
|
+
return True
|
|
229
|
+
if any(line.startswith("… ") and " omitted" in line for line in stripped):
|
|
230
|
+
return True
|
|
231
|
+
if _looks_like_json_output(stripped):
|
|
232
|
+
return True
|
|
233
|
+
if _looks_like_diff(stripped):
|
|
234
|
+
return True
|
|
235
|
+
if _looks_like_test_runner(stripped, lower):
|
|
236
|
+
return True
|
|
237
|
+
if _looks_like_ci_log(stripped):
|
|
238
|
+
return True
|
|
239
|
+
if _looks_like_table(stripped):
|
|
240
|
+
return True
|
|
241
|
+
if _looks_like_path_location_output(stripped):
|
|
242
|
+
return True
|
|
243
|
+
if _looks_like_build_or_package_output(stripped, lower):
|
|
244
|
+
return True
|
|
245
|
+
if any(_line_has_secret_like_key(line) for line in stripped):
|
|
246
|
+
return True
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def _looks_like_json_output(lines: list[str]) -> bool:
|
|
250
|
+
if len(lines) == 1:
|
|
251
|
+
line = lines[0]
|
|
252
|
+
return line.startswith(("{", "[")) and line.endswith(("}", "]")) and ":" in line
|
|
253
|
+
|
|
254
|
+
joined = "\n".join(lines)
|
|
255
|
+
if joined[:1] in ("{", "[") and joined[-1:] in ("}", "]") and ":" in joined:
|
|
256
|
+
return True
|
|
257
|
+
json_like = sum(
|
|
258
|
+
1 for line in lines if line.startswith("{") and line.endswith("}") and ":" in line
|
|
259
|
+
)
|
|
260
|
+
return json_like * 100 >= len(lines) * 70
|
|
261
|
+
|
|
262
|
+
def _looks_like_diff(lines: list[str]) -> bool:
|
|
263
|
+
return (
|
|
264
|
+
any(line.startswith(("diff --git ", "Index: ", "@@ ")) for line in lines)
|
|
265
|
+
or (
|
|
266
|
+
any(line.startswith("--- ") for line in lines)
|
|
267
|
+
and any(line.startswith("+++ ") for line in lines)
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _looks_like_test_runner(lines: list[str], lower: str) -> bool:
|
|
272
|
+
return (
|
|
273
|
+
"pytest" in lower
|
|
274
|
+
or "test session starts" in lower
|
|
275
|
+
or any(line.startswith(("Test Files", "Tests", "FAIL ")) for line in lines)
|
|
276
|
+
or any(line.startswith("test result:") for line in lines)
|
|
277
|
+
or (
|
|
278
|
+
any(line.startswith("Ran ") and " tests in " in line for line in lines)
|
|
279
|
+
and any(line == "OK" or line.startswith("FAILED (") for line in lines)
|
|
280
|
+
)
|
|
281
|
+
or any(line.startswith("Running ") and " tests using " in line for line in lines)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _looks_like_ci_log(lines: list[str]) -> bool:
|
|
285
|
+
return any(
|
|
286
|
+
line.startswith(("##[", "::group::", "::endgroup::", "::error", "::warning"))
|
|
287
|
+
or "\t" in line
|
|
288
|
+
and "T" in line
|
|
289
|
+
and "Z " in line
|
|
290
|
+
for line in lines
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _looks_like_table(lines: list[str]) -> bool:
|
|
294
|
+
pipe_rows = sum(1 for line in lines if "|" in line)
|
|
295
|
+
separator_rows = sum(1 for line in lines if set(line.strip()) <= {"-", "+", "|"})
|
|
296
|
+
return pipe_rows >= 3 and (pipe_rows * 100 >= len(lines) * 55 or separator_rows >= 1)
|
|
297
|
+
|
|
298
|
+
def _looks_like_path_location_output(lines: list[str]) -> bool:
|
|
299
|
+
matches = sum(1 for line in lines if _looks_like_path_location_line(line))
|
|
300
|
+
return matches >= 2 and matches * 100 >= len(lines) * 45
|
|
301
|
+
|
|
302
|
+
def _looks_like_path_location_line(line: str) -> bool:
|
|
303
|
+
parts = line.split(":")
|
|
304
|
+
if len(parts) < 3:
|
|
305
|
+
return False
|
|
306
|
+
return any(part.isdigit() for part in parts[1:3]) and (
|
|
307
|
+
"/" in parts[0] or "." in parts[0]
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def _looks_like_build_or_package_output(lines: list[str], lower: str) -> bool:
|
|
311
|
+
markers = (
|
|
312
|
+
"compiling ",
|
|
313
|
+
"checking ",
|
|
314
|
+
"finished ",
|
|
315
|
+
"could not compile",
|
|
316
|
+
"error[",
|
|
317
|
+
"warning[",
|
|
318
|
+
"downloading ",
|
|
319
|
+
"downloaded ",
|
|
320
|
+
"installing collected packages",
|
|
321
|
+
"successfully installed",
|
|
322
|
+
"added ",
|
|
323
|
+
"packages audited",
|
|
324
|
+
"resolved ",
|
|
325
|
+
"prepared ",
|
|
326
|
+
"installed ",
|
|
327
|
+
"npm warn",
|
|
328
|
+
"npm err",
|
|
329
|
+
)
|
|
330
|
+
return any(marker in lower for marker in markers)
|
|
331
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Rendering and orchestration for repeated log-template compaction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..text import join_preserving_final_newline, score, split_preserving_final_newline
|
|
6
|
+
from .constants import _MAX_SHORT_TUPLES
|
|
7
|
+
from .detector import detect_log_templates
|
|
8
|
+
from .types import LogTemplateSignal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compress_log_template_output(text: str) -> str:
|
|
12
|
+
"""Compact repeated scalar-varied log lines into template/value blocks."""
|
|
13
|
+
|
|
14
|
+
lines, final_newline = split_preserving_final_newline(text)
|
|
15
|
+
signal = detect_log_templates(lines)
|
|
16
|
+
if signal is None:
|
|
17
|
+
return text
|
|
18
|
+
|
|
19
|
+
candidate_lines = _render_template_blocks(lines, signal)
|
|
20
|
+
if candidate_lines is None:
|
|
21
|
+
return text
|
|
22
|
+
|
|
23
|
+
candidate = join_preserving_final_newline(candidate_lines, final_newline)
|
|
24
|
+
if score(candidate) < score(text):
|
|
25
|
+
return candidate
|
|
26
|
+
return text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render_template_blocks(
|
|
30
|
+
lines: list[str], signal: LogTemplateSignal
|
|
31
|
+
) -> list[str] | None:
|
|
32
|
+
output: list[str] = []
|
|
33
|
+
emitted = False
|
|
34
|
+
index = 0
|
|
35
|
+
|
|
36
|
+
while index < len(lines):
|
|
37
|
+
candidate = signal.line_templates[index]
|
|
38
|
+
if candidate is None or candidate.template not in signal.repeated_templates:
|
|
39
|
+
output.append(lines[index])
|
|
40
|
+
index += 1
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
run_template = candidate.template
|
|
44
|
+
run_values: list[tuple[str, ...]] = []
|
|
45
|
+
cursor = index
|
|
46
|
+
while cursor < len(lines):
|
|
47
|
+
run_candidate = signal.line_templates[cursor]
|
|
48
|
+
if run_candidate is None or run_candidate.template != run_template:
|
|
49
|
+
break
|
|
50
|
+
run_values.append(run_candidate.values)
|
|
51
|
+
cursor += 1
|
|
52
|
+
|
|
53
|
+
if len(run_values) < 2:
|
|
54
|
+
output.append(lines[index])
|
|
55
|
+
index += 1
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
output.append(f"… {len(run_values)} templated lines: {run_template}")
|
|
59
|
+
output.append(f" values: {_format_value_tuples(run_values)}")
|
|
60
|
+
emitted = True
|
|
61
|
+
index = cursor
|
|
62
|
+
|
|
63
|
+
if not emitted:
|
|
64
|
+
return None
|
|
65
|
+
return ["log templates: repeated line summary", *output]
|
|
66
|
+
|
|
67
|
+
def _format_value_tuples(values: list[tuple[str, ...]]) -> str:
|
|
68
|
+
if len(values) <= _MAX_SHORT_TUPLES:
|
|
69
|
+
return "; ".join(_format_tuple(value_tuple) for value_tuple in values)
|
|
70
|
+
|
|
71
|
+
omitted = len(values) - 6
|
|
72
|
+
rendered = [_format_tuple(value_tuple) for value_tuple in values[:3]]
|
|
73
|
+
rendered.append(f"… {omitted} omitted")
|
|
74
|
+
rendered.extend(_format_tuple(value_tuple) for value_tuple in values[-3:])
|
|
75
|
+
return "; ".join(rendered)
|
|
76
|
+
|
|
77
|
+
def _format_tuple(values: tuple[str, ...]) -> str:
|
|
78
|
+
return f"({', '.join(values)})"
|