ostruct-cli 0.7.1__py3-none-any.whl → 0.8.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.
Files changed (46) hide show
  1. ostruct/cli/__init__.py +21 -3
  2. ostruct/cli/base_errors.py +1 -1
  3. ostruct/cli/cli.py +66 -1983
  4. ostruct/cli/click_options.py +460 -28
  5. ostruct/cli/code_interpreter.py +238 -0
  6. ostruct/cli/commands/__init__.py +32 -0
  7. ostruct/cli/commands/list_models.py +128 -0
  8. ostruct/cli/commands/quick_ref.py +50 -0
  9. ostruct/cli/commands/run.py +137 -0
  10. ostruct/cli/commands/update_registry.py +71 -0
  11. ostruct/cli/config.py +277 -0
  12. ostruct/cli/cost_estimation.py +134 -0
  13. ostruct/cli/errors.py +310 -6
  14. ostruct/cli/exit_codes.py +1 -0
  15. ostruct/cli/explicit_file_processor.py +548 -0
  16. ostruct/cli/field_utils.py +69 -0
  17. ostruct/cli/file_info.py +42 -9
  18. ostruct/cli/file_list.py +301 -102
  19. ostruct/cli/file_search.py +455 -0
  20. ostruct/cli/file_utils.py +47 -13
  21. ostruct/cli/mcp_integration.py +541 -0
  22. ostruct/cli/model_creation.py +150 -1
  23. ostruct/cli/model_validation.py +204 -0
  24. ostruct/cli/progress_reporting.py +398 -0
  25. ostruct/cli/registry_updates.py +14 -9
  26. ostruct/cli/runner.py +1418 -0
  27. ostruct/cli/schema_utils.py +113 -0
  28. ostruct/cli/services.py +626 -0
  29. ostruct/cli/template_debug.py +748 -0
  30. ostruct/cli/template_debug_help.py +162 -0
  31. ostruct/cli/template_env.py +15 -6
  32. ostruct/cli/template_filters.py +55 -3
  33. ostruct/cli/template_optimizer.py +474 -0
  34. ostruct/cli/template_processor.py +1080 -0
  35. ostruct/cli/template_rendering.py +69 -34
  36. ostruct/cli/token_validation.py +286 -0
  37. ostruct/cli/types.py +78 -0
  38. ostruct/cli/unattended_operation.py +269 -0
  39. ostruct/cli/validators.py +386 -3
  40. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
  41. ostruct_cli-0.8.0.dist-info/METADATA +633 -0
  42. ostruct_cli-0.8.0.dist-info/RECORD +69 -0
  43. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
  44. ostruct_cli-0.7.1.dist-info/METADATA +0 -369
  45. ostruct_cli-0.7.1.dist-info/RECORD +0 -45
  46. {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,748 @@
1
+ """Template debugging infrastructure for ostruct CLI.
2
+
3
+ This module provides debugging capabilities for template expansion and optimization,
4
+ including proper logging configuration and template visibility features.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import click
13
+
14
+
15
+ def configure_debug_logging(
16
+ verbose: bool = False, debug: bool = False
17
+ ) -> None:
18
+ """Configure debug logging system to properly show template expansion.
19
+
20
+ Args:
21
+ verbose: Enable verbose logging (INFO level)
22
+ debug: Enable debug logging (DEBUG level)
23
+ """
24
+ # Configure the root ostruct logger
25
+ logger = logging.getLogger("ostruct")
26
+
27
+ # Remove any existing handlers to avoid duplicates
28
+ for handler in logger.handlers[:]:
29
+ logger.removeHandler(handler)
30
+
31
+ # Create console handler
32
+ handler = logging.StreamHandler()
33
+
34
+ # Set logging level based on flags
35
+ if debug:
36
+ logger.setLevel(logging.DEBUG)
37
+ handler.setLevel(logging.DEBUG)
38
+ formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s")
39
+ elif verbose:
40
+ logger.setLevel(logging.INFO)
41
+ handler.setLevel(logging.INFO)
42
+ formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s")
43
+ else:
44
+ logger.setLevel(logging.WARNING)
45
+ handler.setLevel(logging.WARNING)
46
+ formatter = logging.Formatter("%(levelname)s:%(message)s")
47
+
48
+ handler.setFormatter(formatter)
49
+ logger.addHandler(handler)
50
+
51
+ # Prevent propagation to root logger to avoid duplicate messages
52
+ logger.propagate = False
53
+
54
+
55
+ def log_template_expansion(
56
+ template_content: str,
57
+ context: Dict[str, Any],
58
+ expanded: str,
59
+ template_file: Optional[str] = None,
60
+ ) -> None:
61
+ """Log template expansion with structured debug information.
62
+
63
+ Args:
64
+ template_content: Raw template content
65
+ context: Template context variables
66
+ expanded: Expanded template result
67
+ template_file: Optional template file path
68
+ """
69
+ logger = logging.getLogger(__name__)
70
+
71
+ logger.debug("=== TEMPLATE EXPANSION DEBUG ===")
72
+ logger.debug(f"Template file: {template_file or 'inline'}")
73
+ logger.debug(f"Context variables: {list(context.keys())}")
74
+ logger.debug("Raw template content:")
75
+ logger.debug(template_content)
76
+ logger.debug("Expanded template:")
77
+ logger.debug(expanded)
78
+ logger.debug("=== END TEMPLATE EXPANSION ===")
79
+
80
+
81
+ def show_template_content(
82
+ system_prompt: str,
83
+ user_prompt: str,
84
+ show_templates: bool = False,
85
+ debug: bool = False,
86
+ ) -> None:
87
+ """Show template content with appropriate formatting.
88
+
89
+ Args:
90
+ system_prompt: System prompt content
91
+ user_prompt: User prompt content
92
+ show_templates: Show templates flag
93
+ debug: Debug flag
94
+ """
95
+ logger = logging.getLogger(__name__)
96
+
97
+ if show_templates or debug:
98
+ # Use click.echo for immediate output that bypasses logging
99
+ click.echo("📝 Template Content:", err=True)
100
+ click.echo("=" * 50, err=True)
101
+ click.echo("System Prompt:", err=True)
102
+ click.echo("-" * 20, err=True)
103
+ click.echo(system_prompt, err=True)
104
+ click.echo("\nUser Prompt:", err=True)
105
+ click.echo("-" * 20, err=True)
106
+ click.echo(user_prompt, err=True)
107
+ click.echo("=" * 50, err=True)
108
+
109
+ # Also log for debug mode
110
+ if debug:
111
+ logger.debug("System Prompt:")
112
+ logger.debug("-" * 40)
113
+ logger.debug(system_prompt)
114
+ logger.debug("User Prompt:")
115
+ logger.debug("-" * 40)
116
+ logger.debug(user_prompt)
117
+
118
+
119
+ def show_file_content_expansions(context: Dict[str, Any]) -> None:
120
+ """Show file content expansions for debugging.
121
+
122
+ Args:
123
+ context: Template context containing file information
124
+ """
125
+ logger = logging.getLogger(__name__)
126
+
127
+ logger.debug("📁 File Content Expansions:")
128
+ for key, value in context.items():
129
+ if hasattr(value, "content"): # FileInfo object
130
+ logger.debug(
131
+ f" → {key}: {getattr(value, 'path', 'unknown')} ({len(value.content)} chars)"
132
+ )
133
+ elif isinstance(value, str) and len(value) > 100:
134
+ logger.debug(f" → {key}: {len(value)} chars")
135
+ elif isinstance(value, dict):
136
+ logger.debug(f" → {key}: dict with {len(value)} keys")
137
+ elif isinstance(value, list):
138
+ logger.debug(f" → {key}: list with {len(value)} items")
139
+ else:
140
+ logger.debug(f" → {key}: {type(value).__name__}")
141
+
142
+
143
+ class TemplateDebugger:
144
+ """Template debugging helper for tracking expansion steps."""
145
+
146
+ def __init__(self, enabled: bool = False):
147
+ """Initialize the debugger.
148
+
149
+ Args:
150
+ enabled: Whether debugging is enabled
151
+ """
152
+ self.enabled = enabled
153
+ self.expansion_log: List[Dict[str, Any]] = []
154
+
155
+ def log_expansion_step(
156
+ self,
157
+ step: str,
158
+ before: str,
159
+ after: str,
160
+ context: Optional[Dict[str, Any]] = None,
161
+ ) -> None:
162
+ """Log a template expansion step.
163
+
164
+ Args:
165
+ step: Description of the expansion step
166
+ before: Content before expansion
167
+ after: Content after expansion
168
+ context: Optional context information
169
+ """
170
+ if self.enabled:
171
+ self.expansion_log.append(
172
+ {
173
+ "step": step,
174
+ "before": before,
175
+ "after": after,
176
+ "context": context,
177
+ "timestamp": time.time(),
178
+ }
179
+ )
180
+
181
+ def show_expansion_summary(self) -> None:
182
+ """Show a summary of expansion steps."""
183
+ if not self.enabled or not self.expansion_log:
184
+ return
185
+
186
+ logger = logging.getLogger(__name__)
187
+ logger.debug("🔧 Template Expansion Summary:")
188
+ for step in self.expansion_log:
189
+ logger.debug(f" → {step['step']}")
190
+ if step["context"]:
191
+ logger.debug(f" Variables: {list(step['context'].keys())}")
192
+
193
+ def show_detailed_expansion(self) -> None:
194
+ """Show detailed expansion information for each step."""
195
+ if not self.enabled or not self.expansion_log:
196
+ return
197
+
198
+ logger = logging.getLogger(__name__)
199
+ logger.debug("🔍 Detailed Template Expansion:")
200
+ for i, step_info in enumerate(self.expansion_log, 1):
201
+ logger.debug(f"\n--- Step {i}: {step_info['step']} ---")
202
+ if step_info["before"]:
203
+ logger.debug("Before:")
204
+ before_preview = step_info["before"][:200] + (
205
+ "..." if len(step_info["before"]) > 200 else ""
206
+ )
207
+ logger.debug(before_preview)
208
+ logger.debug("After:")
209
+ after_preview = step_info["after"][:200] + (
210
+ "..." if len(step_info["after"]) > 200 else ""
211
+ )
212
+ logger.debug(after_preview)
213
+ if step_info["context"]:
214
+ logger.debug(f"Context: {list(step_info['context'].keys())}")
215
+
216
+ def get_expansion_stats(self) -> Dict[str, Any]:
217
+ """Get statistics about template expansion.
218
+
219
+ Returns:
220
+ Dictionary with expansion statistics
221
+ """
222
+ if not self.expansion_log:
223
+ return {}
224
+
225
+ total_steps = len(self.expansion_log)
226
+ context_vars = set()
227
+ for step in self.expansion_log:
228
+ if step["context"]:
229
+ context_vars.update(step["context"].keys())
230
+
231
+ return {
232
+ "total_steps": total_steps,
233
+ "unique_variables": len(context_vars),
234
+ "variable_names": sorted(list(context_vars)),
235
+ }
236
+
237
+
238
+ @dataclass
239
+ class FileInspection:
240
+ """Information about a file variable."""
241
+
242
+ path: str
243
+ size: int
244
+ type: str
245
+
246
+
247
+ @dataclass
248
+ class StringInspection:
249
+ """Information about a string variable."""
250
+
251
+ length: int
252
+ multiline: bool
253
+
254
+
255
+ @dataclass
256
+ class ObjectInspection:
257
+ """Information about an object variable."""
258
+
259
+ type: str
260
+ keys: List[str]
261
+
262
+
263
+ @dataclass
264
+ class ContextReport:
265
+ """Report of template context inspection."""
266
+
267
+ files: Dict[str, FileInspection]
268
+ strings: Dict[str, StringInspection]
269
+ objects: Dict[str, ObjectInspection]
270
+
271
+ def __init__(self) -> None:
272
+ self.files = {}
273
+ self.strings = {}
274
+ self.objects = {}
275
+
276
+
277
+ class TemplateContextInspector:
278
+ """Inspector for template variable context."""
279
+
280
+ @staticmethod
281
+ def inspect_context(context: Dict[str, Any]) -> ContextReport:
282
+ """Inspect template context and generate a report.
283
+
284
+ Args:
285
+ context: Template context dictionary
286
+
287
+ Returns:
288
+ ContextReport with inspection results
289
+ """
290
+ report = ContextReport()
291
+
292
+ for key, value in context.items():
293
+ # Check for FileInfo objects (from ostruct file system)
294
+ if hasattr(value, "content") and hasattr(value, "path"):
295
+ # Single FileInfo object
296
+ mime_type = getattr(value, "mime_type", "unknown")
297
+ report.files[key] = FileInspection(
298
+ path=getattr(value, "path", "unknown"),
299
+ size=(
300
+ len(value.content) if hasattr(value, "content") else 0
301
+ ),
302
+ type=mime_type or "unknown",
303
+ )
304
+ elif hasattr(value, "__iter__") and not isinstance(
305
+ value, (str, bytes)
306
+ ):
307
+ # Check if it's a list/collection of FileInfo objects
308
+ try:
309
+ items = list(value)
310
+ if (
311
+ items
312
+ and hasattr(items[0], "content")
313
+ and hasattr(items[0], "path")
314
+ ):
315
+ # FileInfoList - collect info about all files
316
+ total_size = sum(
317
+ (
318
+ len(item.content)
319
+ if hasattr(item, "content")
320
+ else 0
321
+ )
322
+ for item in items
323
+ )
324
+ paths = [
325
+ getattr(item, "path", "unknown") for item in items
326
+ ]
327
+ report.files[key] = FileInspection(
328
+ path=f"{len(items)} files: {', '.join(paths[:3])}{'...' if len(paths) > 3 else ''}",
329
+ size=total_size,
330
+ type="file_collection",
331
+ )
332
+ else:
333
+ # Regular list/collection
334
+ report.objects[key] = ObjectInspection(
335
+ type=f"list[{len(items)}]",
336
+ keys=[
337
+ str(i) for i in range(min(5, len(items)))
338
+ ], # Show first 5 indices
339
+ )
340
+ except (TypeError, AttributeError):
341
+ # Fallback for non-iterable or problematic objects
342
+ report.objects[key] = ObjectInspection(
343
+ type=type(value).__name__, keys=[]
344
+ )
345
+ elif isinstance(value, str):
346
+ report.strings[key] = StringInspection(
347
+ length=len(value), multiline="\n" in value
348
+ )
349
+ elif isinstance(value, dict):
350
+ report.objects[key] = ObjectInspection(
351
+ type="dict",
352
+ keys=list(value.keys())[:10], # Show first 10 keys
353
+ )
354
+ else:
355
+ # Other types (int, bool, etc.)
356
+ report.objects[key] = ObjectInspection(
357
+ type=type(value).__name__, keys=[]
358
+ )
359
+
360
+ return report
361
+
362
+
363
+ def display_context_summary(context: Dict[str, Any]) -> None:
364
+ """Display a summary of template context variables.
365
+
366
+ Args:
367
+ context: Template context dictionary
368
+ """
369
+ click.echo("📋 Template Context Summary:", err=True)
370
+ click.echo("=" * 50, err=True)
371
+
372
+ inspector = TemplateContextInspector()
373
+ report = inspector.inspect_context(context)
374
+
375
+ if report.files:
376
+ click.echo(f"📄 Files ({len(report.files)}):", err=True)
377
+ for name, info in report.files.items():
378
+ size_str = f"{info.size:,} chars" if info.size > 0 else "empty"
379
+ click.echo(
380
+ f" → {name}: {info.path} ({size_str}, {info.type})", err=True
381
+ )
382
+
383
+ if report.strings:
384
+ click.echo(f"📝 Strings ({len(report.strings)}):", err=True)
385
+ for name, string_info in report.strings.items():
386
+ multiline_str = " (multiline)" if string_info.multiline else ""
387
+ click.echo(
388
+ f" → {name}: {string_info.length} chars{multiline_str}",
389
+ err=True,
390
+ )
391
+
392
+ if report.objects:
393
+ click.echo(f"🗂️ Objects ({len(report.objects)}):", err=True)
394
+ for name, object_info in report.objects.items():
395
+ if object_info.keys:
396
+ keys_preview = ", ".join(object_info.keys[:5])
397
+ if len(object_info.keys) > 5:
398
+ keys_preview += "..."
399
+ click.echo(
400
+ f" → {name}: {object_info.type} ({keys_preview})",
401
+ err=True,
402
+ )
403
+ else:
404
+ click.echo(f" → {name}: {object_info.type}", err=True)
405
+
406
+ click.echo("=" * 50, err=True)
407
+
408
+
409
+ def display_context_detailed(context: Dict[str, Any]) -> None:
410
+ """Display detailed template context with content previews.
411
+
412
+ Args:
413
+ context: Template context dictionary
414
+ """
415
+ click.echo("📋 Detailed Template Context:", err=True)
416
+ click.echo("=" * 50, err=True)
417
+
418
+ inspector = TemplateContextInspector()
419
+ report = inspector.inspect_context(context)
420
+
421
+ # Show files with content preview
422
+ if report.files:
423
+ click.echo("📄 File Variables:", err=True)
424
+ for name, info in report.files.items():
425
+ click.echo(f"\n {name}:", err=True)
426
+ click.echo(f" Path: {info.path}", err=True)
427
+ click.echo(f" Size: {info.size:,} chars", err=True)
428
+ click.echo(f" Type: {info.type}", err=True)
429
+
430
+ # Show content preview for single files
431
+ if name in context and hasattr(context[name], "content"):
432
+ content = context[name].content
433
+ if len(content) > 200:
434
+ preview = content[:200] + "..."
435
+ else:
436
+ preview = content
437
+ click.echo(f" Preview: {repr(preview)}", err=True)
438
+
439
+ # Show strings with content preview
440
+ if report.strings:
441
+ click.echo("\n📝 String Variables:", err=True)
442
+ for name, string_info in report.strings.items():
443
+ click.echo(f"\n {name}:", err=True)
444
+ click.echo(f" Length: {string_info.length} chars", err=True)
445
+ click.echo(f" Multiline: {string_info.multiline}", err=True)
446
+
447
+ # Show content preview
448
+ content = context[name]
449
+ if len(content) > 100:
450
+ preview = content[:100] + "..."
451
+ else:
452
+ preview = content
453
+ click.echo(f" Preview: {repr(preview)}", err=True)
454
+
455
+ # Show objects with structure
456
+ if report.objects:
457
+ click.echo("\n🗂️ Object Variables:", err=True)
458
+ for name, object_info in report.objects.items():
459
+ click.echo(f"\n {name}:", err=True)
460
+ click.echo(f" Type: {object_info.type}", err=True)
461
+ if object_info.keys:
462
+ click.echo(
463
+ f" Keys/Indices: {', '.join(object_info.keys)}",
464
+ err=True,
465
+ )
466
+
467
+ click.echo("=" * 50, err=True)
468
+
469
+
470
+ def show_pre_optimization_template(template_content: str) -> None:
471
+ """Display template content before optimization is applied.
472
+
473
+ Args:
474
+ template_content: Raw template content before optimization
475
+ """
476
+ click.echo("🔧 Template Before Optimization:", err=True)
477
+ click.echo("=" * 50, err=True)
478
+ click.echo(template_content, err=True)
479
+ click.echo("=" * 50, err=True)
480
+
481
+
482
+ def show_optimization_diff(original: str, optimized: str) -> None:
483
+ """Show template optimization changes in a readable diff format.
484
+
485
+ Args:
486
+ original: Original template content
487
+ optimized: Optimized template content
488
+ """
489
+ click.echo("🔄 Template Optimization Changes:", err=True)
490
+ click.echo("=" * 50, err=True)
491
+
492
+ # Simple line-by-line comparison
493
+ original_lines = original.split("\n")
494
+ optimized_lines = optimized.split("\n")
495
+
496
+ # Show basic statistics
497
+ click.echo(
498
+ f"Original: {len(original_lines)} lines, {len(original)} chars",
499
+ err=True,
500
+ )
501
+ click.echo(
502
+ f"Optimized: {len(optimized_lines)} lines, {len(optimized)} chars",
503
+ err=True,
504
+ )
505
+
506
+ if original == optimized:
507
+ click.echo("✅ No optimization changes made", err=True)
508
+ click.echo("=" * 50, err=True)
509
+ return
510
+
511
+ click.echo("\nChanges:", err=True)
512
+
513
+ # Find differences line by line
514
+ max_lines = max(len(original_lines), len(optimized_lines))
515
+ changes_found = False
516
+
517
+ for i in range(max_lines):
518
+ orig_line = original_lines[i] if i < len(original_lines) else ""
519
+ opt_line = optimized_lines[i] if i < len(optimized_lines) else ""
520
+
521
+ if orig_line != opt_line:
522
+ changes_found = True
523
+ click.echo(f" Line {i + 1}:", err=True)
524
+ if orig_line:
525
+ click.echo(f" - {orig_line}", err=True)
526
+ if opt_line:
527
+ click.echo(f" + {opt_line}", err=True)
528
+
529
+ if not changes_found:
530
+ # If no line-by-line differences but content differs, show character-level diff
531
+ click.echo(
532
+ " Content differs but not at line level (whitespace/formatting changes)",
533
+ err=True,
534
+ )
535
+
536
+ click.echo("=" * 50, err=True)
537
+
538
+
539
+ def detect_undefined_variables(
540
+ template_content: str, context: Dict[str, Any]
541
+ ) -> List[str]:
542
+ """Detect undefined variables in template content.
543
+
544
+ Args:
545
+ template_content: Template content to analyze
546
+ context: Available context variables
547
+
548
+ Returns:
549
+ List of undefined variable names
550
+ """
551
+ # This is a simple implementation - could be enhanced with proper Jinja2 AST parsing
552
+ import re
553
+
554
+ # Find all variable references in the template
555
+ variable_pattern = r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[|\}]"
556
+ variables = re.findall(variable_pattern, template_content)
557
+
558
+ # Check which variables are not in context
559
+ undefined_vars = []
560
+ for var in set(variables):
561
+ if var not in context:
562
+ undefined_vars.append(var)
563
+
564
+ return undefined_vars
565
+
566
+
567
+ @dataclass
568
+ class OptimizationStep:
569
+ """Information about a single optimization step."""
570
+
571
+ name: str
572
+ before: str
573
+ after: str
574
+ reason: str
575
+ timestamp: float
576
+ chars_changed: int = 0
577
+
578
+ def __post_init__(self) -> None:
579
+ """Calculate character changes after initialization."""
580
+ self.chars_changed = len(self.after) - len(self.before)
581
+
582
+
583
+ class OptimizationStepTracker:
584
+ """Tracker for detailed optimization step analysis."""
585
+
586
+ def __init__(self, enabled: bool = False):
587
+ """Initialize the optimization step tracker.
588
+
589
+ Args:
590
+ enabled: Whether step tracking is enabled
591
+ """
592
+ self.enabled = enabled
593
+ self.steps: List[OptimizationStep] = []
594
+
595
+ def log_step(
596
+ self,
597
+ step_name: str,
598
+ before: str,
599
+ after: str,
600
+ reason: str,
601
+ ) -> None:
602
+ """Log an optimization step.
603
+
604
+ Args:
605
+ step_name: Name/description of the optimization step
606
+ before: Content before this step
607
+ after: Content after this step
608
+ reason: Explanation of why this step was applied
609
+ """
610
+ if self.enabled:
611
+ step = OptimizationStep(
612
+ name=step_name,
613
+ before=before,
614
+ after=after,
615
+ reason=reason,
616
+ timestamp=time.time(),
617
+ )
618
+ self.steps.append(step)
619
+
620
+ def show_step_summary(self) -> None:
621
+ """Show a summary of optimization steps."""
622
+ if not self.enabled or not self.steps:
623
+ return
624
+
625
+ click.echo("🔧 Optimization Steps Summary:", err=True)
626
+ click.echo("=" * 50, err=True)
627
+
628
+ total_chars_changed = 0
629
+ for i, step in enumerate(self.steps, 1):
630
+ total_chars_changed += step.chars_changed
631
+ change_indicator = (
632
+ "📈"
633
+ if step.chars_changed > 0
634
+ else "📉" if step.chars_changed < 0 else "📊"
635
+ )
636
+
637
+ click.echo(f" {i}. {step.name}: {step.reason}", err=True)
638
+ if step.before != step.after:
639
+ click.echo(
640
+ f" {change_indicator} Changed: {len(step.before)} → {len(step.after)} chars ({step.chars_changed:+d})",
641
+ err=True,
642
+ )
643
+ else:
644
+ click.echo(" 📊 No changes made", err=True)
645
+
646
+ click.echo(
647
+ f"\n📊 Total: {total_chars_changed:+d} characters changed",
648
+ err=True,
649
+ )
650
+ click.echo("=" * 50, err=True)
651
+
652
+ def show_detailed_steps(self) -> None:
653
+ """Show detailed information for each optimization step."""
654
+ if not self.enabled or not self.steps:
655
+ return
656
+
657
+ click.echo("🔍 Detailed Optimization Steps:", err=True)
658
+ click.echo("=" * 50, err=True)
659
+
660
+ for i, step in enumerate(self.steps, 1):
661
+ click.echo(f"\n--- Step {i}: {step.name} ---", err=True)
662
+ click.echo(f"Reason: {step.reason}", err=True)
663
+ click.echo(f"Character change: {step.chars_changed:+d}", err=True)
664
+
665
+ if step.before != step.after:
666
+ click.echo("Changes:", err=True)
667
+ _show_step_diff(step.before, step.after)
668
+ else:
669
+ click.echo("✅ No changes made", err=True)
670
+
671
+ click.echo("=" * 50, err=True)
672
+
673
+ def get_step_stats(self) -> Dict[str, Any]:
674
+ """Get statistics about optimization steps.
675
+
676
+ Returns:
677
+ Dictionary with step statistics
678
+ """
679
+ if not self.steps:
680
+ return {}
681
+
682
+ total_steps = len(self.steps)
683
+ total_chars_changed = sum(step.chars_changed for step in self.steps)
684
+ steps_with_changes = sum(
685
+ 1 for step in self.steps if step.before != step.after
686
+ )
687
+
688
+ return {
689
+ "total_steps": total_steps,
690
+ "steps_with_changes": steps_with_changes,
691
+ "total_chars_changed": total_chars_changed,
692
+ "step_names": [step.name for step in self.steps],
693
+ }
694
+
695
+
696
+ def _show_step_diff(before: str, after: str) -> None:
697
+ """Show a simple diff between before and after content.
698
+
699
+ Args:
700
+ before: Content before changes
701
+ after: Content after changes
702
+ """
703
+ before_lines = before.split("\n")
704
+ after_lines = after.split("\n")
705
+
706
+ max_lines = max(len(before_lines), len(after_lines))
707
+ changes_shown = 0
708
+ max_changes = 5 # Limit output for readability
709
+
710
+ for i in range(max_lines):
711
+ if changes_shown >= max_changes:
712
+ click.echo(
713
+ f" ... ({max_lines - i} more lines not shown)", err=True
714
+ )
715
+ break
716
+
717
+ before_line = before_lines[i] if i < len(before_lines) else ""
718
+ after_line = after_lines[i] if i < len(after_lines) else ""
719
+
720
+ if before_line != after_line:
721
+ changes_shown += 1
722
+ click.echo(f" Line {i + 1}:", err=True)
723
+ if before_line:
724
+ click.echo(f" - {before_line}", err=True)
725
+ if after_line:
726
+ click.echo(f" + {after_line}", err=True)
727
+
728
+
729
+ def show_optimization_steps(
730
+ steps: List[OptimizationStep], detail_level: str = "summary"
731
+ ) -> None:
732
+ """Show optimization steps with specified detail level.
733
+
734
+ Args:
735
+ steps: List of optimization steps
736
+ detail_level: Level of detail ("summary" or "detailed")
737
+ """
738
+ if not steps:
739
+ click.echo("ℹ️ No optimization steps were recorded", err=True)
740
+ return
741
+
742
+ tracker = OptimizationStepTracker(enabled=True)
743
+ tracker.steps = steps
744
+
745
+ if detail_level == "detailed":
746
+ tracker.show_detailed_steps()
747
+ else:
748
+ tracker.show_step_summary()