contextops 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.
@@ -0,0 +1,424 @@
1
+ """
2
+ CLI Renderer.
3
+
4
+ Pretty-prints AnalysisResult to the terminal.
5
+ All rendering is derived from JSON (AnalysisResult.to_dict()) — the CLI
6
+ is a view layer only. The JSON is the source of truth.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+ from contextops.core.models import AnalysisResult
14
+ from typing import Any
15
+
16
+
17
+ # ── ANSI color codes ────────────────────────────────────────────────────
18
+
19
+ class _Colors:
20
+ RESET = "\033[0m"
21
+ BOLD = "\033[1m"
22
+ DIM = "\033[2m"
23
+ RED = "\033[31m"
24
+ GREEN = "\033[32m"
25
+ YELLOW = "\033[33m"
26
+ BLUE = "\033[34m"
27
+ MAGENTA = "\033[35m"
28
+ CYAN = "\033[36m"
29
+ WHITE = "\033[37m"
30
+ BG_RED = "\033[41m"
31
+ BG_GREEN = "\033[42m"
32
+ BG_YELLOW = "\033[43m"
33
+
34
+
35
+ C = _Colors
36
+
37
+
38
+ def _score_color(score: int) -> str:
39
+ """Pick a color based on the score range."""
40
+ if score >= 80:
41
+ return C.GREEN
42
+ elif score >= 60:
43
+ return C.YELLOW
44
+ elif score >= 40:
45
+ return C.RED
46
+ else:
47
+ return C.BG_RED + C.WHITE
48
+
49
+
50
+ def _score_label(score: int) -> str:
51
+ """Human-readable label for the score."""
52
+ if score >= 90:
53
+ return "EXCELLENT"
54
+ elif score >= 80:
55
+ return "GOOD"
56
+ elif score >= 60:
57
+ return "NEEDS WORK"
58
+ elif score >= 40:
59
+ return "POOR"
60
+ else:
61
+ return "CRITICAL"
62
+
63
+
64
+ def _bar(value: float, max_value: float, width: int = 20) -> str:
65
+ """Render a simple horizontal bar chart."""
66
+ filled = int((value / max(max_value, 1)) * width)
67
+ filled = min(filled, width)
68
+ return "#" * filled + "-" * (width - filled)
69
+
70
+
71
+ def render(result: AnalysisResult, use_json: bool = False, explain: bool = False) -> str:
72
+ """
73
+ Render an AnalysisResult for terminal display.
74
+
75
+ Args:
76
+ result: The analysis result to render.
77
+ use_json: If True, output raw JSON instead of pretty formatting.
78
+
79
+ Returns:
80
+ A formatted string ready for print().
81
+ """
82
+ if use_json:
83
+ return json.dumps(result.to_dict(), indent=2)
84
+
85
+ lines: list[str] = []
86
+ data = result.to_dict()
87
+ score = data["score"]
88
+
89
+ # ── Header ──────────────────────────────────────────────────────
90
+ lines.append("")
91
+ lines.append(f"{C.BOLD}{C.CYAN}+{'=' * 50}+{C.RESET}")
92
+ lines.append(f"{C.BOLD}{C.CYAN}| CONTEXTOPS -- Context Analysis |{C.RESET}")
93
+ lines.append(f"{C.BOLD}{C.CYAN}+{'=' * 50}+{C.RESET}")
94
+ lines.append("")
95
+
96
+ # ── Score ────────────────────────────────────────────────────────
97
+ color = _score_color(score)
98
+ label = _score_label(score)
99
+ lines.append(f" {C.BOLD}Context Score:{C.RESET} {color}{C.BOLD} {score} / 100 {C.RESET} {C.DIM}({label}){C.RESET}")
100
+ lines.append("")
101
+
102
+ # ── Score Breakdown ─────────────────────────────────────────────
103
+ bd = data["score_breakdown"]
104
+ lines.append(f" {C.BOLD}Score Breakdown:{C.RESET}")
105
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
106
+
107
+ penalties = [
108
+ ("Redundancy", bd["redundancy_penalty"], 30, C.RED),
109
+ ("Density", bd["density_penalty"], 30, C.YELLOW),
110
+ ("Structure", bd["structure_penalty"], 20, C.MAGENTA),
111
+ ("Concentration", bd["concentration_penalty"], 20, C.BLUE),
112
+ ]
113
+ for name, value, max_val, clr in penalties:
114
+ bar = _bar(value, max_val)
115
+ lines.append(
116
+ f" {clr} {name:<12}{C.RESET} "
117
+ f"-{value:>5.1f} / {max_val} {C.DIM}{bar}{C.RESET}"
118
+ )
119
+
120
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
121
+ lines.append(f" {C.BOLD} Total Penalty{C.RESET} -{bd['total_penalty']:>5.1f} / 100")
122
+ lines.append("")
123
+
124
+ # ── Token Breakdown ─────────────────────────────────────────────
125
+ tb = data["token_breakdown"]
126
+ lines.append(f" {C.BOLD}Token Breakdown:{C.RESET}")
127
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
128
+ if tb['wasted_tokens'] > 0:
129
+ lines.append(f" {C.DIM}Cost Model: ~15 penalty points per 1,000 wasted tokens (capped at 30){C.RESET}")
130
+ lines.append(f" Total tokens: {C.BOLD}{tb['total_tokens']:,}{C.RESET}")
131
+ lines.append(f" Wasted tokens: {C.RED}{tb['wasted_tokens']:,}{C.RESET}")
132
+ lines.append(f" Estimated cost: ${tb['estimated_cost_usd']:.4f}")
133
+ lines.append("")
134
+
135
+ if tb["by_type"]:
136
+ lines.append(f" {C.DIM}By Type:{C.RESET}")
137
+ for ctx_type, tokens in sorted(tb["by_type"].items(), key=lambda x: -x[1]):
138
+ pct = (tokens / max(1, tb["total_tokens"])) * 100
139
+ bar = _bar(tokens, tb["total_tokens"], width=15)
140
+ lines.append(
141
+ f" {ctx_type:<12} {tokens:>6,} tokens ({pct:>4.1f}%) {C.DIM}{bar}{C.RESET}"
142
+ )
143
+ lines.append("")
144
+
145
+ # ── Shadow Signals (Phase 2 Preview) ────────────────────────────
146
+ if "density_signal" in data:
147
+ ds = data["density_signal"]
148
+ effect = data.get("density_effect", "shadow")
149
+ if effect == "shadow":
150
+ lines.append(f" {C.BOLD}{C.CYAN}Shadow Metrics (Phase 2 Preview):{C.RESET}")
151
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
152
+
153
+ lines.append(f" Format Overhead: {ds['format_overhead']:.2f}")
154
+ lines.append(f" Whitespace Waste: {ds['whitespace_waste']:.2f}")
155
+ lines.append(f" Entropy Compression: {ds['entropy_compression']:.2f}")
156
+ lines.append(f" {C.BOLD}Total Density Signal:{C.RESET} {C.YELLOW}{ds['total_density_signal']:.2f}{C.RESET}")
157
+ lines.append(f" {C.DIM}(This is a shadow metric and does not affect the score yet){C.RESET}")
158
+ lines.append("")
159
+
160
+
161
+ # ── Findings ────────────────────────────────────────────────────
162
+ if not explain:
163
+ redundancy_findings = data["findings"]["redundancy"]
164
+ structure_findings = data["findings"]["structure"]
165
+
166
+ if redundancy_findings or structure_findings:
167
+ lines.append(f" {C.BOLD}Findings:{C.RESET}")
168
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
169
+
170
+ for f in redundancy_findings:
171
+ icon = "[~]" if f["classification"] == "expected_overlap" else "[!]"
172
+ lines.append(
173
+ f" {icon} {C.YELLOW}{f['classification'].upper()}{C.RESET}: "
174
+ f"{f['detail']}"
175
+ )
176
+ lines.append(
177
+ f" {C.DIM}similarity: {f['similarity']:.0%} | "
178
+ f"waste: {f['waste_tokens']:,} tokens{C.RESET}"
179
+ )
180
+
181
+ for f in structure_findings:
182
+ sev_color = C.RED if f["severity"] in ("high", "critical") else C.YELLOW
183
+ lines.append(
184
+ f" [S] {sev_color}{f['issue']}{C.RESET}: "
185
+ f"{f['type']} is {f['actual_ratio']:.0%} of context"
186
+ )
187
+
188
+ if redundancy_findings or structure_findings:
189
+ lines.append("")
190
+
191
+ # ── Recommendations ─────────────────────────────────────────────
192
+ if not explain:
193
+ recs = data["recommendations"]
194
+ if recs:
195
+ lines.append(f" {C.BOLD}{C.GREEN}Recommendations:{C.RESET}")
196
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
197
+
198
+ for i, rec in enumerate(recs, 1):
199
+ sev_color = C.RED if rec["severity"] in ("high", "critical") else C.YELLOW
200
+ lines.append(
201
+ f" {C.BOLD}{i}. {sev_color}{rec['issue']}{C.RESET}"
202
+ )
203
+ lines.append(
204
+ f" {C.GREEN}Impact: {rec['impact']}{C.RESET} | "
205
+ f"Save: {rec['token_savings']:,} tokens"
206
+ )
207
+ lines.append(
208
+ f" {C.CYAN}Fix: {rec['fix']}{C.RESET}"
209
+ )
210
+ lines.append("")
211
+
212
+ # ── Explain Mode ────────────────────────────────────────────────
213
+ if explain:
214
+ # Deterministic sorting
215
+ sorted_recs = sorted(
216
+ result.recommendations,
217
+ key=lambda r: (-r.impact_score, -r.token_savings, r.issue)
218
+ )
219
+
220
+ if sorted_recs:
221
+ lines.append(f" {C.BOLD}{C.MAGENTA}Top Score Drivers:{C.RESET}")
222
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
223
+
224
+ top_recs = sorted_recs[:3]
225
+ for i, r in enumerate(top_recs, 1):
226
+ lines.append(f" {C.BOLD}{i}. {r.issue}{C.RESET}")
227
+ lines.append(f" Impact: {C.RED}-{round(r.impact_score, 1)}{C.RESET}")
228
+ lines.append("")
229
+
230
+ hidden_count = len(sorted_recs) - 3
231
+ if hidden_count > 0:
232
+ lines.append(f" {C.DIM}+ {hidden_count} minor issues hidden{C.RESET}")
233
+ lines.append("")
234
+
235
+ # Potential Score Summary
236
+ top_driver = sorted_recs[0]
237
+ potential_score = min(100, score + round(top_driver.impact_score))
238
+
239
+ lines.append(f" {C.BOLD}{C.GREEN}Why This Score Is Not Higher{C.RESET}")
240
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
241
+ lines.append(f" Potential Score: {C.GREEN}{C.BOLD}{potential_score}{C.RESET}")
242
+ lines.append(f" Current Score: {color}{C.BOLD}{score}{C.RESET}")
243
+ lines.append("")
244
+ lines.append(f" {C.BOLD}Largest Opportunity:{C.RESET}")
245
+ lines.append(f" {C.CYAN}{top_driver.fix}{C.RESET}")
246
+ lines.append("")
247
+ lines.append(f" {C.BOLD}Expected Gain:{C.RESET}")
248
+ lines.append(f" {C.GREEN}+{round(top_driver.impact_score, 1)} points{C.RESET}")
249
+ if top_driver.token_savings > 0:
250
+ lines.append(f" {C.GREEN}{top_driver.token_savings:,} tokens saved{C.RESET}")
251
+ lines.append("")
252
+
253
+ # ── Footer ──────────────────────────────────────────────────────
254
+ lines.append(f" {C.DIM}contextops v{data['metadata'].get('version', '0.1.0')} "
255
+ f"| {data['metadata'].get('item_count', 0)} items analyzed{C.RESET}")
256
+ lines.append("")
257
+ lines.append(f" {C.YELLOW}{C.BOLD}LIMITATION:{C.RESET}")
258
+ lines.append(f" {C.DIM}This tool measures structural density, not semantic usefulness.{C.RESET}")
259
+ lines.append(f" {C.DIM}A high score does not guarantee the LLM has the right facts to answer.{C.RESET}")
260
+ lines.append("")
261
+
262
+ return "\n".join(lines)
263
+
264
+
265
+ def render_stability(report: Any) -> str:
266
+ """
267
+ Render a StabilityReport for terminal display.
268
+ Expects report of type contextops.api.stability.StabilityReport.
269
+ """
270
+ lines: list[str] = []
271
+
272
+ lines.append("")
273
+ lines.append(f"{C.BOLD}{C.CYAN}+{'=' * 50}+{C.RESET}")
274
+ lines.append(f"{C.BOLD}{C.CYAN}| CONTEXTOPS -- Stability Report |{C.RESET}")
275
+ lines.append(f"{C.BOLD}{C.CYAN}+{'=' * 50}+{C.RESET}")
276
+ lines.append("")
277
+
278
+ lines.append(f" {C.BOLD}Base Score:{C.RESET} {report.base_score}")
279
+ lines.append(f" {C.BOLD}Base Tokens:{C.RESET} {report.base_tokens:,}")
280
+ lines.append(f" {C.BOLD}Base Waste Tokens:{C.RESET} {report.base_waste_tokens:,}")
281
+ lines.append("")
282
+
283
+ for inv in report.invariants:
284
+ lines.append(f" {C.BOLD}{inv.name}{C.RESET}")
285
+
286
+ if inv.passed:
287
+ lines.append(f" {C.GREEN}PASS{C.RESET}")
288
+ else:
289
+ lines.append(f" {C.RED}FAIL{C.RESET}")
290
+
291
+ for key, value in inv.diagnostic_info.items():
292
+ lines.append(f" {C.DIM}{key}: {value}{C.RESET}")
293
+
294
+ lines.append("")
295
+
296
+ score = report.score_percentage
297
+ color = _score_color(score)
298
+ passed_count = sum(1 for inv in report.invariants if inv.passed)
299
+ total_count = len(report.invariants)
300
+
301
+ if score >= 90:
302
+ confidence = "High"
303
+ elif score >= 70:
304
+ confidence = "Medium"
305
+ else:
306
+ confidence = "Low"
307
+
308
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
309
+ lines.append(f" {C.BOLD}Stability Score:{C.RESET}")
310
+ lines.append(f" {color}{C.BOLD}{score}%{C.RESET} {C.DIM}({passed_count}/{total_count} passed){C.RESET}")
311
+ lines.append(f" {C.BOLD}Confidence: {confidence}{C.RESET}")
312
+ lines.append("")
313
+
314
+ return "\n".join(lines)
315
+
316
+
317
+ def render_diff(diff: Any) -> str:
318
+ """
319
+ Render a ContextDiffResult for terminal display.
320
+ Expects diff of type contextops.api.diff.ContextDiffResult.
321
+ """
322
+ lines: list[str] = []
323
+
324
+ lines.append("")
325
+ lines.append(f"{C.BOLD}{C.CYAN}+{'=' * 50}+{C.RESET}")
326
+ lines.append(f"{C.BOLD}{C.CYAN}| CONTEXTOPS -- Context Diff |{C.RESET}")
327
+ lines.append(f"{C.BOLD}{C.CYAN}+{'=' * 50}+{C.RESET}")
328
+ lines.append("")
329
+
330
+ # ── Score Section ───────────────────────────────────────────────
331
+ score_a = diff.result_a.score
332
+ score_b = diff.result_b.score
333
+ score_color = C.GREEN if diff.score_delta > 0 else (C.RED if diff.score_delta < 0 else C.RESET)
334
+ score_sign = "+" if diff.score_delta > 0 else ""
335
+ lines.append(f" {C.BOLD}Score:{C.RESET} {score_a} -> {score_b} "
336
+ f"({score_color}{score_sign}{diff.score_delta}{C.RESET})")
337
+ lines.append("")
338
+
339
+ # ── Tokens / Cost ───────────────────────────────────────────────
340
+ lines.append(f" {C.BOLD}Tokens / Cost{C.RESET}")
341
+ tok_a = diff.result_a.token_breakdown.total_tokens
342
+ tok_b = diff.result_b.token_breakdown.total_tokens
343
+ tok_color = C.GREEN if diff.token_delta < 0 else (C.RED if diff.token_delta > 0 else C.RESET)
344
+ tok_sign = "+" if diff.token_delta > 0 else ""
345
+ lines.append(f" Tokens: {tok_a:,} -> {tok_b:,} "
346
+ f"({tok_color}{tok_sign}{diff.token_delta:,}{C.RESET})")
347
+
348
+ cost_a = diff.result_a.token_breakdown.estimated_cost_usd
349
+ cost_b = diff.result_b.token_breakdown.estimated_cost_usd
350
+ cost_color = C.GREEN if diff.cost_delta < 0 else (C.RED if diff.cost_delta > 0 else C.RESET)
351
+ cost_sign = "+" if diff.cost_delta > 0 else ""
352
+ lines.append(f" Cost: ${cost_a:.4f} -> ${cost_b:.4f} "
353
+ f"({cost_color}{cost_sign}${abs(diff.cost_delta):.4f}{C.RESET})")
354
+ lines.append("")
355
+
356
+ # ── Structure Changes ───────────────────────────────────────────
357
+ lines.append(f" {C.BOLD}Structure Changes (Penalties){C.RESET}")
358
+ for key, delta in diff.structure_delta.items():
359
+ if abs(delta) < 0.01:
360
+ continue
361
+ color = C.GREEN if delta < 0 else C.RED
362
+ sign = "+" if delta > 0 else ""
363
+ lines.append(f" {key.capitalize()}: {color}{sign}{delta:.2f}{C.RESET}")
364
+
365
+ if all(abs(v) < 0.01 for v in diff.structure_delta.values()):
366
+ lines.append(f" {C.DIM}No significant structure changes.{C.RESET}")
367
+ lines.append("")
368
+
369
+ # ── Recommendation Lifecycle ────────────────────────────────────
370
+ lines.append(f" {C.BOLD}Recommendation Lifecycle{C.RESET}")
371
+
372
+ if diff.resolved_recommendations:
373
+ lines.append(f" {C.GREEN}Resolved:{C.RESET}")
374
+ for r in diff.resolved_recommendations:
375
+ lines.append(f" {C.GREEN}[-] {r.issue}{C.RESET}")
376
+
377
+ if diff.new_recommendations:
378
+ lines.append(f" {C.RED}New:{C.RESET}")
379
+ for r in diff.new_recommendations:
380
+ lines.append(f" {C.RED}[+] {r.issue}{C.RESET}")
381
+
382
+ if diff.persisting_recommendations:
383
+ lines.append(f" {C.DIM}Persisting:{C.RESET}")
384
+ for r in diff.persisting_recommendations:
385
+ lines.append(f" {C.DIM}[~] {r.issue}{C.RESET}")
386
+
387
+ if not (diff.resolved_recommendations or diff.new_recommendations or diff.persisting_recommendations):
388
+ lines.append(f" {C.DIM}No recommendations.{C.RESET}")
389
+
390
+ lines.append("")
391
+
392
+ # ── Net Impact Summary ──────────────────────────────────────────
393
+ lines.append(f" {C.DIM}{'-' * 48}{C.RESET}")
394
+ lines.append(f" {C.BOLD}Net Impact Summary:{C.RESET}")
395
+
396
+ # Generate impact bullets
397
+ if diff.token_delta < 0:
398
+ lines.append(f" {C.GREEN}\u2714 Reduced token usage ({diff.token_delta:,}){C.RESET}")
399
+ elif diff.token_delta > 0:
400
+ lines.append(f" {C.RED}\u2716 Increased token usage (+{diff.token_delta:,}){C.RESET}")
401
+
402
+ if diff.score_delta > 0:
403
+ lines.append(f" {C.GREEN}\u2714 Improved score (+{diff.score_delta}){C.RESET}")
404
+ elif diff.score_delta < 0:
405
+ lines.append(f" {C.RED}\u2716 Score degraded ({diff.score_delta}){C.RESET}")
406
+
407
+ for key, delta in diff.structure_delta.items():
408
+ if delta > 0.05: # significant penalty increase
409
+ lines.append(f" {C.RED}\u2716 {key.replace('_', ' ')} penalty increased (+{delta:.2f}){C.RESET}")
410
+ elif delta < -0.05: # significant penalty decrease
411
+ lines.append(f" {C.GREEN}\u2714 {key.replace('_', ' ')} penalty decreased ({delta:.2f}){C.RESET}")
412
+
413
+ lines.append("")
414
+
415
+ if diff.net_impact == "IMPROVEMENT":
416
+ lines.append(f" Overall: {C.BG_GREEN}{C.WHITE}{C.BOLD} IMPROVEMENT {C.RESET}")
417
+ elif diff.net_impact == "DEGRADATION":
418
+ lines.append(f" Overall: {C.BG_RED}{C.WHITE}{C.BOLD} DEGRADATION {C.RESET}")
419
+ else:
420
+ lines.append(f" Overall: {C.BG_YELLOW}{C.WHITE}{C.BOLD} NEUTRAL {C.RESET}")
421
+
422
+ lines.append("")
423
+
424
+ return "\n".join(lines)
@@ -0,0 +1 @@
1
+ # Core subpackage
@@ -0,0 +1,61 @@
1
+ """
2
+ Configuration models for ContextOps.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class ContextOpsConfig:
15
+ """
16
+ Configuration for ContextOps analyzers.
17
+
18
+ Thresholds represent the maximum allowed ratio of context for a given type.
19
+ """
20
+ retrieval_max_ratio: float = 0.70
21
+ system_max_ratio: float = 0.50
22
+ memory_max_ratio: float = 0.50
23
+ tool_max_ratio: float = 0.60
24
+
25
+ # "strict" means default thresholds are used (standardized score).
26
+ # "custom" means user has overridden thresholds.
27
+ mode: str = "strict"
28
+ version: str = "1.0"
29
+
30
+ @classmethod
31
+ def default(cls) -> ContextOpsConfig:
32
+ return cls()
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: dict[str, Any]) -> ContextOpsConfig:
36
+ config = cls()
37
+ has_custom = False
38
+
39
+ if "retrieval_max_ratio" in data:
40
+ config.retrieval_max_ratio = float(data["retrieval_max_ratio"])
41
+ has_custom = True
42
+ if "system_max_ratio" in data:
43
+ config.system_max_ratio = float(data["system_max_ratio"])
44
+ has_custom = True
45
+ if "memory_max_ratio" in data:
46
+ config.memory_max_ratio = float(data["memory_max_ratio"])
47
+ has_custom = True
48
+ if "tool_max_ratio" in data:
49
+ config.tool_max_ratio = float(data["tool_max_ratio"])
50
+ has_custom = True
51
+
52
+ if has_custom:
53
+ config.mode = "custom"
54
+
55
+ return config
56
+
57
+ @classmethod
58
+ def from_file(cls, path: str | Path) -> ContextOpsConfig:
59
+ with open(path, "r", encoding="utf-8") as f:
60
+ data = json.load(f)
61
+ return cls.from_dict(data)