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.
- ostruct/cli/__init__.py +21 -3
- ostruct/cli/base_errors.py +1 -1
- ostruct/cli/cli.py +66 -1983
- ostruct/cli/click_options.py +460 -28
- ostruct/cli/code_interpreter.py +238 -0
- ostruct/cli/commands/__init__.py +32 -0
- ostruct/cli/commands/list_models.py +128 -0
- ostruct/cli/commands/quick_ref.py +50 -0
- ostruct/cli/commands/run.py +137 -0
- ostruct/cli/commands/update_registry.py +71 -0
- ostruct/cli/config.py +277 -0
- ostruct/cli/cost_estimation.py +134 -0
- ostruct/cli/errors.py +310 -6
- ostruct/cli/exit_codes.py +1 -0
- ostruct/cli/explicit_file_processor.py +548 -0
- ostruct/cli/field_utils.py +69 -0
- ostruct/cli/file_info.py +42 -9
- ostruct/cli/file_list.py +301 -102
- ostruct/cli/file_search.py +455 -0
- ostruct/cli/file_utils.py +47 -13
- ostruct/cli/mcp_integration.py +541 -0
- ostruct/cli/model_creation.py +150 -1
- ostruct/cli/model_validation.py +204 -0
- ostruct/cli/progress_reporting.py +398 -0
- ostruct/cli/registry_updates.py +14 -9
- ostruct/cli/runner.py +1418 -0
- ostruct/cli/schema_utils.py +113 -0
- ostruct/cli/services.py +626 -0
- ostruct/cli/template_debug.py +748 -0
- ostruct/cli/template_debug_help.py +162 -0
- ostruct/cli/template_env.py +15 -6
- ostruct/cli/template_filters.py +55 -3
- ostruct/cli/template_optimizer.py +474 -0
- ostruct/cli/template_processor.py +1080 -0
- ostruct/cli/template_rendering.py +69 -34
- ostruct/cli/token_validation.py +286 -0
- ostruct/cli/types.py +78 -0
- ostruct/cli/unattended_operation.py +269 -0
- ostruct/cli/validators.py +386 -3
- {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
- ostruct_cli-0.8.0.dist-info/METADATA +633 -0
- ostruct_cli-0.8.0.dist-info/RECORD +69 -0
- {ostruct_cli-0.7.1.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.7.1.dist-info/METADATA +0 -369
- ostruct_cli-0.7.1.dist-info/RECORD +0 -45
- {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()
|