slimctx 0.1.0__py3-none-any.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.
slimctx/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """slimctx — a zero-dependency, fully-reversible context compression layer
2
+ for AI agents.
3
+
4
+ from slimctx import Pipeline, Config
5
+
6
+ pipe = Pipeline(Config(target_tokens=32_000))
7
+ result = pipe.compress(messages) # OpenAI/Anthropic style dicts
8
+ result.savings_ratio # e.g. 0.72
9
+ pipe.retrieve("a1b2c3...") # byte-exact original back
10
+
11
+ Guarantees:
12
+ * pure Python stdlib — no models, no downloads, no network, ever
13
+ * every lossy transform stores the original first (always reversible)
14
+ * deterministic + memoized -> stable prefixes -> provider caches hit
15
+ * error/warning content is never dropped by any compressor
16
+ """
17
+
18
+ from .pipeline import Config, Pipeline, Result
19
+ from .store import MemoryStore, SqliteStore
20
+ from .tokens import estimate_tokens
21
+
22
+ __version__ = "0.1.0"
23
+ __all__ = [
24
+ "Pipeline",
25
+ "Config",
26
+ "Result",
27
+ "MemoryStore",
28
+ "SqliteStore",
29
+ "estimate_tokens",
30
+ ]
@@ -0,0 +1,196 @@
1
+ """Code compression: structural skeletons instead of raw truncation.
2
+
3
+ For Python we use the real AST: keep imports, class/function signatures,
4
+ docstring first-lines, and module-level assignments; elide bodies with a
5
+ line-count note. The model keeps the *map* of the file — usually what it
6
+ actually needs — and can retrieve the full source via the store marker.
7
+
8
+ Query-aware body retention: when the caller passes the user's query, the
9
+ few function bodies most relevant to it are kept verbatim while the rest
10
+ are elided — the model sees the whole map plus the exact code it is being
11
+ asked about. (Blanket skeletonization forces a retrieval round-trip for
12
+ the one function the user actually cares about.)
13
+
14
+ For other languages we fall back to a brace/keyword skeleton: keep lines
15
+ that open declarations, drop deep indentation blocks with elision notes.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast
21
+ import re
22
+ from typing import List, Optional
23
+
24
+ from .relevance import Bm25, _stem, tokenize
25
+
26
+ MAX_VERBATIM_BODIES = 3
27
+ DUPLICATE_JACCARD = 0.8
28
+
29
+ _PY_HINTS = re.compile(r"^\s*(def |class |import |from |@|if __name__)", re.MULTILINE)
30
+ _DECL_RE = re.compile(
31
+ r"^\s*(?:export\s+)?(?:public|private|protected|static|async|final|abstract)?\s*"
32
+ r"(?:function|class|interface|struct|enum|impl|fn|func|def|type|const|var|let|"
33
+ r"void|int|string|bool|public|private)\b"
34
+ )
35
+
36
+
37
+ def looks_like_code(text: str) -> bool:
38
+ lines = text.splitlines()
39
+ if len(lines) < 8:
40
+ return False
41
+ if len(_PY_HINTS.findall(text)) >= 3:
42
+ return True
43
+ decl_hits = sum(1 for ln in lines[:80] if _DECL_RE.match(ln))
44
+ brace_lines = sum(1 for ln in lines[:80] if ln.rstrip().endswith(("{", "}", ");")))
45
+ return decl_hits + brace_lines >= len(lines[:80]) * 0.25
46
+
47
+
48
+ def compress_code(text: str, query: str = "") -> Optional[str]:
49
+ result = _python_skeleton(text, query)
50
+ if result is not None:
51
+ return result
52
+ return _generic_skeleton(text)
53
+
54
+
55
+ def _relevant_function_ids(tree: ast.AST, lines: List[str], query: str) -> set:
56
+ """ids of function nodes whose bodies should be kept verbatim."""
57
+ if not query:
58
+ return set()
59
+ funcs = [
60
+ n for n in ast.walk(tree)
61
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
62
+ ]
63
+ if not funcs:
64
+ return set()
65
+ sources = [
66
+ "\n".join(lines[f.lineno - 1 : getattr(f, "end_lineno", f.lineno)])
67
+ for f in funcs
68
+ ]
69
+ bm25 = Bm25(sources)
70
+ scored = sorted(
71
+ ((bm25.score(i, query), i) for i in range(len(funcs))), reverse=True
72
+ )
73
+ # Relative gate: an absolute score threshold fails when the query term
74
+ # appears in every function (IDF -> 0). What matters is which bodies
75
+ # score *best*, as long as there is any signal at all. Diversity gate:
76
+ # a body near-identical to one already kept adds no information
77
+ # (common in codebases with many boilerplate-similar functions).
78
+ best = scored[0][0]
79
+ if best <= 1e-9:
80
+ return set()
81
+ keep: set = set()
82
+ kept_tokens: List[set] = []
83
+ for score, i in scored:
84
+ if len(keep) >= MAX_VERBATIM_BODIES or score < best * 0.5:
85
+ break
86
+ toks = {_stem(t) for t in tokenize(sources[i])}
87
+ if any(_jaccard(toks, prev) > DUPLICATE_JACCARD for prev in kept_tokens):
88
+ continue
89
+ keep.add(id(funcs[i]))
90
+ kept_tokens.append(toks)
91
+ return keep
92
+
93
+
94
+ def _jaccard(a: set, b: set) -> float:
95
+ if not a or not b:
96
+ return 0.0
97
+ return len(a & b) / len(a | b)
98
+
99
+
100
+ def _python_skeleton(text: str, query: str = "") -> Optional[str]:
101
+ try:
102
+ tree = ast.parse(text)
103
+ except (SyntaxError, ValueError, RecursionError, MemoryError):
104
+ return None
105
+
106
+ lines = text.splitlines()
107
+ out: List[str] = []
108
+ verbatim_ids = _relevant_function_ids(tree, lines, query)
109
+
110
+ def emit_docstring(node: ast.AST, indent: str) -> None:
111
+ doc = ast.get_docstring(node, clean=True)
112
+ if doc:
113
+ first = doc.splitlines()[0]
114
+ out.append(f'{indent}"""{first}"""')
115
+
116
+ def signature_lines(node: ast.AST) -> List[str]:
117
+ # decorators + the def/class line(s) up to the colon
118
+ start = min(
119
+ [d.lineno for d in getattr(node, "decorator_list", [])] + [node.lineno]
120
+ )
121
+ body_start = node.body[0].lineno if node.body else node.lineno + 1
122
+ sig = []
123
+ for ln in lines[start - 1 : body_start - 1]:
124
+ sig.append(ln)
125
+ if ln.rstrip().endswith(":"):
126
+ break
127
+ return sig
128
+
129
+ def body_size(node: ast.AST) -> int:
130
+ return (getattr(node, "end_lineno", None) or node.lineno) - node.lineno + 1
131
+
132
+ def visit(node: ast.AST, depth: int) -> None:
133
+ indent = " " * depth
134
+ for child in ast.iter_child_nodes(node):
135
+ if isinstance(child, (ast.Import, ast.ImportFrom)) and depth == 0:
136
+ out.append(lines[child.lineno - 1])
137
+ elif isinstance(child, ast.ClassDef):
138
+ out.extend(signature_lines(child))
139
+ emit_docstring(child, indent + " ")
140
+ visit(child, depth + 1)
141
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
142
+ if id(child) in verbatim_ids:
143
+ start = min(
144
+ [d.lineno for d in child.decorator_list] + [child.lineno]
145
+ )
146
+ end = getattr(child, "end_lineno", child.lineno)
147
+ out.extend(lines[start - 1 : end])
148
+ else:
149
+ out.extend(signature_lines(child))
150
+ emit_docstring(child, indent + " ")
151
+ out.append(f"{indent} ... # {body_size(child)} lines")
152
+ elif isinstance(child, (ast.Assign, ast.AnnAssign)) and depth == 0:
153
+ src_line = lines[child.lineno - 1]
154
+ out.append(src_line if len(src_line) < 120 else src_line[:117] + "...")
155
+
156
+ visit(tree, 0)
157
+ if not out:
158
+ return None
159
+ skeleton = "\n".join(out)
160
+ if len(skeleton) >= len(text) * 0.85:
161
+ return None # not worth it
162
+ total = len(lines)
163
+ return f"# skeleton of {total}-line python file (bodies elided)\n{skeleton}"
164
+
165
+
166
+ def _generic_skeleton(text: str) -> Optional[str]:
167
+ lines = text.splitlines()
168
+ out: List[str] = []
169
+ elided = 0
170
+
171
+ def flush() -> None:
172
+ nonlocal elided
173
+ if elided:
174
+ out.append(f" ... // {elided} lines elided")
175
+ elided = 0
176
+
177
+ for ln in lines:
178
+ stripped = ln.strip()
179
+ indent = len(ln) - len(ln.lstrip())
180
+ is_structural = (
181
+ _DECL_RE.match(ln)
182
+ or stripped.startswith(("//", "/*", "*", "#", "import ", "package ", "using "))
183
+ or (indent == 0 and stripped in ("}", "};"))
184
+ )
185
+ if is_structural:
186
+ flush()
187
+ out.append(ln)
188
+ elif indent <= 2 and stripped.endswith("{"):
189
+ flush()
190
+ out.append(ln)
191
+ else:
192
+ elided += 1
193
+ flush()
194
+ if not out or len("\n".join(out)) >= len(text) * 0.85:
195
+ return None
196
+ return f"// skeleton of {len(lines)}-line file (bodies elided)\n" + "\n".join(out)
@@ -0,0 +1,234 @@
1
+ """JSON compression: lossless tabularization first, relevance-ranked
2
+ row selection second.
3
+
4
+ Stage 1 (lossless): a homogeneous array of objects repeats every key name
5
+ in every element. Re-emitting it as a header + rows table removes that
6
+ redundancy without dropping a single value:
7
+
8
+ [{"id": 1, "name": "a", "ok": true}, ...] ->
9
+ #table cols=id|name|ok rows=200
10
+ 1|a|true
11
+ ...
12
+
13
+ Nested objects are flattened to dotted columns (meta.region) when uniform.
14
+
15
+ Stage 2 (lossy, only if a row budget is given and exceeded): rank rows and
16
+ keep the best under budget. A row is kept when it (a) contains an error /
17
+ salient value, (b) is a statistical outlier on any numeric column (>2
18
+ sigma), (c) is in the head or tail (positional anchors), or (d) scores
19
+ high BM25 relevance against the query. Kept rows keep original order.
20
+
21
+ Everything else (heterogeneous arrays, scalars, deep trees) is minified
22
+ (no whitespace) — also lossless.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import math
29
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
30
+
31
+ from .relevance import Bm25, salience_score
32
+
33
+ CORE_FIELD_THRESHOLD = 0.8 # key must appear in >=80% of rows to be a column
34
+ HEAD_KEEP = 3
35
+ TAIL_KEEP = 2
36
+
37
+
38
+ def try_parse(text: str) -> Optional[Any]:
39
+ stripped = text.strip()
40
+ if not stripped or stripped[0] not in "[{":
41
+ return None
42
+ try:
43
+ return json.loads(stripped)
44
+ except (json.JSONDecodeError, ValueError, RecursionError):
45
+ # RecursionError: pathologically nested input (e.g. 100k open
46
+ # brackets) must degrade to "not JSON", not crash the pipeline.
47
+ return None
48
+
49
+
50
+ def _flatten(obj: Dict[str, Any], prefix: str = "", max_depth: int = 2) -> Dict[str, Any]:
51
+ flat: Dict[str, Any] = {}
52
+ for k, v in obj.items():
53
+ key = f"{prefix}{k}"
54
+ if isinstance(v, dict) and max_depth > 0 and 0 < len(v) <= 6:
55
+ flat.update(_flatten(v, f"{key}.", max_depth - 1))
56
+ else:
57
+ flat[key] = v
58
+ return flat
59
+
60
+
61
+ def _find_object_array(data: Any) -> Optional[Tuple[List[Dict[str, Any]], Optional[str]]]:
62
+ """Return (array, wrapper_key) if data is/contains an array of objects."""
63
+ if isinstance(data, list):
64
+ if len(data) >= 2 and all(isinstance(x, dict) for x in data):
65
+ return data, None
66
+ return None
67
+ if isinstance(data, dict):
68
+ # common API shapes: {"items": [...]}, {"results": [...]}, etc.
69
+ candidates = [
70
+ (k, v) for k, v in data.items()
71
+ if isinstance(v, list) and len(v) >= 2 and all(isinstance(x, dict) for x in v)
72
+ ]
73
+ if len(candidates) == 1:
74
+ k, v = candidates[0]
75
+ return v, k
76
+ return None
77
+
78
+
79
+ def _cell(value: Any) -> str:
80
+ if value is None:
81
+ return ""
82
+ if isinstance(value, bool):
83
+ return "true" if value else "false"
84
+ if isinstance(value, (int, float)):
85
+ return json.dumps(value)
86
+ if isinstance(value, str):
87
+ return value.replace("|", "\\|").replace("\n", "\\n")
88
+ return json.dumps(value, separators=(",", ":"))
89
+
90
+
91
+ def _numeric_outlier_rows(rows: List[Dict[str, Any]], cols: Sequence[str]) -> set:
92
+ """Indices of rows that are >2 sigma from the mean on any numeric column."""
93
+ outliers: set = set()
94
+ for col in cols:
95
+ values = []
96
+ for i, row in enumerate(rows):
97
+ v = row.get(col)
98
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
99
+ values.append((i, float(v)))
100
+ if len(values) < 8:
101
+ continue
102
+ nums = [v for _, v in values]
103
+ mean = sum(nums) / len(nums)
104
+ var = sum((x - mean) ** 2 for x in nums) / len(nums)
105
+ std = math.sqrt(var)
106
+ if std == 0:
107
+ continue
108
+ for i, v in values:
109
+ if abs(v - mean) > 2 * std:
110
+ outliers.add(i)
111
+ return outliers
112
+
113
+
114
+ def tabularize(
115
+ text: str,
116
+ query: str = "",
117
+ max_rows: Optional[int] = None,
118
+ ) -> Optional[Tuple[str, bool]]:
119
+ """Compress a JSON string. Returns (compressed, was_lossy) or None if
120
+ the input is not JSON we can improve."""
121
+ data = try_parse(text)
122
+ if data is None:
123
+ return None
124
+
125
+ found = _find_object_array(data)
126
+ if found is None:
127
+ minified = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
128
+ if len(minified) < len(text) * 0.9:
129
+ return minified, False
130
+ return None
131
+
132
+ rows_raw, wrapper = found
133
+ rows = [_flatten(r) for r in rows_raw]
134
+
135
+ # Determine core columns (present in >= threshold of rows).
136
+ freq: Dict[str, int] = {}
137
+ for row in rows:
138
+ for k in row:
139
+ freq[k] = freq.get(k, 0) + 1
140
+ n = len(rows)
141
+ cols = [k for k, c in freq.items() if c >= n * CORE_FIELD_THRESHOLD]
142
+ if len(cols) < 2:
143
+ return None # too heterogeneous to tabularize profitably
144
+ # Stable column order: order of first appearance.
145
+ seen_order: List[str] = []
146
+ for row in rows:
147
+ for k in row:
148
+ if k in cols and k not in seen_order:
149
+ seen_order.append(k)
150
+ cols = seen_order
151
+
152
+ # Constant-column extraction (lossless): a column whose value is
153
+ # identical in every row is stated once in a legend, not repeated
154
+ # per row. Common in API payloads (repo, branch, region, type...).
155
+ consts: Dict[str, Any] = {}
156
+ for col in list(cols):
157
+ if freq.get(col) == n:
158
+ first = rows[0].get(col)
159
+ if all(row.get(col) == first for row in rows):
160
+ consts[col] = first
161
+ cols.remove(col)
162
+ if len(cols) < 1:
163
+ return None
164
+
165
+ kept_indices = list(range(n))
166
+ was_lossy = False
167
+ if max_rows is not None and n > max_rows:
168
+ kept_indices = _select_rows(rows, cols, query, max_rows)
169
+ was_lossy = True
170
+
171
+ lines = []
172
+ label = f" key={wrapper}" if wrapper else ""
173
+ shown = len(kept_indices)
174
+ row_note = f"rows={n}" if not was_lossy else f"rows={shown}of{n}"
175
+ lines.append(f"#table{label} cols={'|'.join(cols)} {row_note}")
176
+ if consts:
177
+ legend = " | ".join(f"{k}={_cell(v)}" for k, v in consts.items())
178
+ lines.append(f"#every-row: {legend}")
179
+ prev = -1
180
+ for i in kept_indices:
181
+ if was_lossy and prev >= 0 and i != prev + 1:
182
+ lines.append(f"…({i - prev - 1} rows omitted)…")
183
+ row = rows[i]
184
+ cells = [_cell(row.get(c)) for c in cols]
185
+ extras = {k: v for k, v in row.items() if k not in cols and k not in consts}
186
+ line = "|".join(cells)
187
+ if extras:
188
+ line += " +" + json.dumps(extras, separators=(",", ":"), ensure_ascii=False)
189
+ lines.append(line)
190
+ prev = i
191
+ return "\n".join(lines), was_lossy
192
+
193
+
194
+ def _select_rows(
195
+ rows: List[Dict[str, Any]], cols: Sequence[str], query: str, max_rows: int
196
+ ) -> List[int]:
197
+ n = len(rows)
198
+ keep: set = set(range(min(HEAD_KEEP, n))) | set(range(max(0, n - TAIL_KEEP), n))
199
+
200
+ row_texts = [json.dumps(r, ensure_ascii=False) for r in rows]
201
+ # Salience pins a row only when it is *distinctive*: a keyword that
202
+ # appears in most rows (e.g. `timeout=30` in every signature) carries
203
+ # no information about which rows matter.
204
+ salient = [i for i, txt in enumerate(row_texts) if salience_score(txt) >= 0.75]
205
+ if len(salient) <= max(3, n // 5):
206
+ keep.update(salient)
207
+ keep |= _numeric_outlier_rows(rows, cols)
208
+
209
+ if query:
210
+ bm25 = Bm25(row_texts)
211
+ scored = sorted(
212
+ ((bm25.score(i, query), i) for i in range(n) if i not in keep),
213
+ reverse=True,
214
+ )
215
+ for score, i in scored:
216
+ if score <= 0.15:
217
+ break
218
+ # Strong matches always earn a slot, even past the row budget —
219
+ # the row that answers the query is the last thing to sacrifice.
220
+ if score >= 0.5 or len(keep) < max_rows:
221
+ keep.add(i)
222
+ elif len(keep) >= max_rows:
223
+ break
224
+
225
+ # Fill remaining budget with evenly spaced samples for coverage.
226
+ if len(keep) < max_rows:
227
+ remaining = [i for i in range(n) if i not in keep]
228
+ want = max_rows - len(keep)
229
+ if remaining and want > 0:
230
+ step = max(1, len(remaining) // want)
231
+ for i in remaining[::step][:want]:
232
+ keep.add(i)
233
+
234
+ return sorted(keep)
@@ -0,0 +1,132 @@
1
+ """Log compression via template mining (Drain-lite).
2
+
3
+ Agent tool output is dominated by *repeated* log lines that differ only in
4
+ volatile fields (timestamps, IDs, durations). Generic text compressors
5
+ score and drop individual sentences; they can't exploit this structure.
6
+ We can:
7
+
8
+ 2026-07-19T10:00:01Z GET /api/users/8231 200 12ms
9
+ 2026-07-19T10:00:02Z GET /api/users/9917 200 9ms
10
+ ... x 1430 more ...
11
+
12
+ becomes one line:
13
+
14
+ <TS> GET /api/users/<NUM> 200 <NUM>ms [x1432]
15
+
16
+ Algorithm:
17
+ 1. Mask volatile tokens per line (timestamps, UUIDs, hex, numbers, IPs).
18
+ 2. Group lines by masked template.
19
+ 3. Emit each template once with its count and 1-2 verbatim examples.
20
+ 4. High-salience lines (errors, warnings, tracebacks) are NEVER collapsed —
21
+ they pass through verbatim, in original order, because they are the
22
+ signal the model is being asked to find.
23
+
24
+ Lossy — so the original is always placed in the store first.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from collections import OrderedDict
31
+ from dataclasses import dataclass, field
32
+ from typing import List
33
+
34
+ from .relevance import salience_score
35
+
36
+ _VOLATILE = [
37
+ (re.compile(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?"), "<TS>"),
38
+ (re.compile(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b"), "<UUID>"),
39
+ (re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b"), "<IP>"),
40
+ (re.compile(r"\b[0-9a-fA-F]{12,}\b"), "<HEX>"),
41
+ # No trailing \b: numbers fused to units ("12ms", "512Mi") must mask
42
+ # too, or every line becomes its own template.
43
+ (re.compile(r"\d+(?:\.\d+)?"), "<NUM>"),
44
+ ]
45
+
46
+ # A template must repeat at least this many times before we collapse it;
47
+ # below that, collapsing costs more tokens (marker overhead) than it saves.
48
+ MIN_REPEAT = 3
49
+
50
+
51
+ def _template_of(line: str) -> str:
52
+ for pattern, token in _VOLATILE:
53
+ line = pattern.sub(token, line)
54
+ return line
55
+
56
+
57
+ @dataclass
58
+ class _Group:
59
+ template: str
60
+ count: int = 0
61
+ first_example: str = ""
62
+ last_example: str = ""
63
+ first_index: int = 0
64
+ salient_lines: List[str] = field(default_factory=list)
65
+
66
+
67
+ def looks_like_log(text: str) -> bool:
68
+ """Heuristic: many short lines, high proportion with timestamps/levels."""
69
+ lines = [ln for ln in text.splitlines() if ln.strip()]
70
+ if len(lines) < 10:
71
+ return False
72
+ hits = 0
73
+ probe = lines[:50]
74
+ level_re = re.compile(r"\b(INFO|DEBUG|WARN|WARNING|ERROR|FATAL|TRACE)\b")
75
+ for ln in probe:
76
+ if _VOLATILE[0][0].search(ln) or level_re.search(ln):
77
+ hits += 1
78
+ return hits >= len(probe) * 0.4
79
+
80
+
81
+ def compress_log(text: str, max_examples_per_template: int = 1) -> str:
82
+ """Collapse repeated log templates; keep salient lines verbatim."""
83
+ lines = text.splitlines()
84
+ groups: "OrderedDict[str, _Group]" = OrderedDict()
85
+ salient: List[tuple] = [] # (index, line) — kept verbatim, in order
86
+
87
+ for i, line in enumerate(lines):
88
+ if not line.strip():
89
+ continue
90
+ if salience_score(line) >= 0.6:
91
+ salient.append((i, line))
92
+ continue
93
+ tpl = _template_of(line)
94
+ grp = groups.get(tpl)
95
+ if grp is None:
96
+ grp = _Group(template=tpl, first_example=line, first_index=i)
97
+ groups[tpl] = grp
98
+ grp.count += 1
99
+ grp.last_example = line
100
+
101
+ out: List[str] = []
102
+ emitted_salient = 0
103
+
104
+ # Interleave: emit groups in first-appearance order, and salient lines
105
+ # at their original positions relative to the groups around them.
106
+ events: List[tuple] = []
107
+ for grp in groups.values():
108
+ events.append((grp.first_index, "group", grp))
109
+ for idx, line in salient:
110
+ events.append((idx, "salient", line))
111
+ events.sort(key=lambda e: e[0])
112
+
113
+ for _, kind, payload in events:
114
+ if kind == "salient":
115
+ out.append(payload)
116
+ emitted_salient += 1
117
+ else:
118
+ grp = payload
119
+ if grp.count < MIN_REPEAT:
120
+ out.append(grp.first_example)
121
+ if grp.count == 2:
122
+ out.append(grp.last_example)
123
+ else:
124
+ out.append(f"{grp.template} [x{grp.count}]")
125
+ if max_examples_per_template >= 1:
126
+ out.append(f" e.g. {grp.first_example}")
127
+
128
+ header = (
129
+ f"[log summary: {len(lines)} lines -> {len(out)} shown; "
130
+ f"{len(groups)} unique patterns; {emitted_salient} error/warn lines kept verbatim]"
131
+ )
132
+ return header + "\n" + "\n".join(out)