pdd-cli 0.0.45__py3-none-any.whl → 0.0.118__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 (195) hide show
  1. pdd/__init__.py +40 -8
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -1,222 +1,264 @@
1
- from typing import Optional, Tuple
2
- from datetime import datetime
3
- try:
4
- from datetime import UTC
5
- except ImportError:
6
- # Python < 3.11 compatibility
7
- from datetime import timezone
8
- UTC = timezone.utc
9
- from io import StringIO
10
- import os
1
+ from __future__ import annotations
2
+
11
3
  import glob
4
+ import hashlib
5
+ import io
12
6
  import csv
13
-
7
+ import os
8
+ from typing import Optional, List, Dict, Tuple, Callable
14
9
  from pydantic import BaseModel, Field
15
- from rich import print
16
- from rich.progress import track
10
+ from rich.console import Console
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
17
12
 
18
- from .load_prompt_template import load_prompt_template
13
+ # Internal imports based on package structure
19
14
  from .llm_invoke import llm_invoke
15
+ from .load_prompt_template import load_prompt_template
20
16
  from . import DEFAULT_TIME
21
17
 
22
- class FileSummary(BaseModel):
23
- file_summary: str = Field(description="The summary of the file")
18
+ console = Console()
24
19
 
25
- def validate_csv_format(csv_content: str) -> bool:
26
- """Validate CSV has required columns and proper format."""
27
- try:
28
- if not csv_content or csv_content.isspace():
29
- return False
30
- reader = csv.DictReader(StringIO(csv_content.lstrip()))
31
- if not reader.fieldnames:
32
- return False
33
- required_columns = {'full_path', 'file_summary', 'date'}
34
- if not all(col in reader.fieldnames for col in required_columns):
35
- return False
36
- try:
37
- first_row = next(reader, None)
38
- if not first_row:
39
- return True
40
- return all(key in first_row for key in required_columns)
41
- except csv.Error:
42
- return False
43
- except Exception:
44
- return False
45
-
46
- def normalize_path(path: str) -> str:
47
- """Normalize path for consistent comparison."""
48
- return os.path.normpath(path.strip().strip('"').strip())
49
-
50
- def parse_date(date_str: str) -> datetime:
51
- """Parse date string to datetime with proper error handling."""
52
- try:
53
- dt = datetime.fromisoformat(date_str.strip())
54
- return dt if dt.tzinfo else dt.replace(tzinfo=UTC)
55
- except Exception:
56
- return datetime.now(UTC)
57
-
58
- def parse_existing_csv(csv_content: str, verbose: bool = False) -> dict:
59
- """Parse existing CSV file and return normalized data."""
60
- existing_data = {}
61
- try:
62
- # Clean the CSV content by removing leading/trailing whitespace from each line
63
- cleaned_lines = [line.strip() for line in csv_content.splitlines()]
64
- cleaned_content = '\n'.join(cleaned_lines)
65
-
66
- reader = csv.DictReader(StringIO(cleaned_content))
67
- for row in reader:
68
- try:
69
- normalized_path = normalize_path(row['full_path'])
70
- existing_data[normalized_path] = {
71
- 'file_summary': row['file_summary'].strip().strip('"'),
72
- 'date': row['date'].strip()
73
- }
74
- if verbose:
75
- print(f"[green]Parsed existing entry for: {normalized_path}[/green]")
76
- except Exception as e:
77
- if verbose:
78
- print(f"[yellow]Warning: Skipping invalid CSV row: {str(e)}[/yellow]")
79
- except Exception as e:
80
- if verbose:
81
- print(f"[yellow]Warning: Error parsing CSV: {str(e)}[/yellow]")
82
- raise ValueError("Invalid CSV file format.")
83
- return existing_data
20
+ class FileSummary(BaseModel):
21
+ """Pydantic model for structured LLM output."""
22
+ file_summary: str = Field(..., description="A concise summary of the file contents.")
84
23
 
85
24
  def summarize_directory(
86
25
  directory_path: str,
87
26
  strength: float,
88
27
  temperature: float,
89
- verbose: bool,
90
28
  time: float = DEFAULT_TIME,
91
- csv_file: Optional[str] = None
29
+ verbose: bool = False,
30
+ csv_file: Optional[str] = None,
31
+ progress_callback: Optional[Callable[[int, int], None]] = None
92
32
  ) -> Tuple[str, float, str]:
93
33
  """
94
- Summarize files in a directory and generate a CSV containing the summaries.
34
+ Summarizes files in a directory using an LLM, with caching based on content hashes.
95
35
 
96
- Parameters:
97
- directory_path (str): Path to the directory to summarize with wildcard (e.g., /path/to/directory/*.py)
98
- strength (float): Between 0 and 1 that is the strength of the LLM model to use.
99
- temperature (float): Controls the randomness of the LLM's output.
100
- verbose (bool): Whether to print out the details of the function.
101
- time (float): Time budget for LLM calls.
102
- csv_file (Optional[str]): Current CSV file contents if it already exists.
36
+ Args:
37
+ directory_path: Path to the directory/files (supports wildcards, e.g., 'src/*.py').
38
+ strength: Float (0-1) indicating LLM model strength.
39
+ temperature: Float controlling LLM randomness.
40
+ time: Float (0-1) controlling thinking effort.
41
+ verbose: Whether to print detailed logs.
42
+ csv_file: Existing CSV content string to check for cache hits.
43
+ progress_callback: Optional callback for progress updates (current, total).
103
44
 
104
45
  Returns:
105
- Tuple[str, float, str]: A tuple containing:
106
- - csv_output (str): Updated CSV content with 'full_path', 'file_summary', and 'date'.
107
- - total_cost (float): Total cost of the LLM runs.
108
- - model_name (str): Name of the LLM model used.
46
+ Tuple containing:
47
+ - csv_output (str): The updated CSV content.
48
+ - total_cost (float): Total cost of LLM operations.
49
+ - model_name (str): Name of the model used (from the last successful call).
109
50
  """
110
- try:
111
- if not isinstance(directory_path, str) or not directory_path:
112
- raise ValueError("Invalid 'directory_path'.")
113
- if not (0.0 <= strength <= 1.0):
114
- raise ValueError("Invalid 'strength' value.")
115
- if not isinstance(temperature, (int, float)) or temperature < 0:
116
- raise ValueError("Invalid 'temperature' value.")
117
- if not isinstance(verbose, bool):
118
- raise ValueError("Invalid 'verbose' value.")
119
-
120
- prompt_template = load_prompt_template("summarize_file_LLM")
121
- if not prompt_template:
122
- raise FileNotFoundError("Prompt template 'summarize_file_LLM.prompt' not found.")
123
-
124
- csv_output = "full_path,file_summary,date\n"
125
- total_cost = 0.0
126
- model_name = "None"
127
-
128
- existing_data = {}
129
- if csv_file:
130
- if not validate_csv_format(csv_file):
131
- raise ValueError("Invalid CSV file format.")
132
- existing_data = parse_existing_csv(csv_file, verbose)
133
-
134
- # Get list of files first to ensure consistent order
135
- files = sorted(glob.glob(directory_path, recursive=True))
136
- if not files:
137
- if verbose:
138
- print("[yellow]No files found.[/yellow]")
139
- return csv_output, total_cost, model_name
51
+
52
+ # Step 1: Input Validation
53
+ if not isinstance(directory_path, str) or not directory_path:
54
+ raise ValueError("Invalid 'directory_path'.")
55
+ if not (0.0 <= strength <= 1.0):
56
+ raise ValueError("Invalid 'strength' value.")
57
+ if not (isinstance(temperature, (int, float)) and temperature >= 0):
58
+ raise ValueError("Invalid 'temperature' value.")
59
+ if not isinstance(verbose, bool):
60
+ raise ValueError("Invalid 'verbose' value.")
61
+
62
+ # Parse existing CSV if provided to validate format and get cached entries
63
+ existing_data: Dict[str, Dict[str, str]] = {}
64
+ if csv_file:
65
+ try:
66
+ f = io.StringIO(csv_file)
67
+ reader = csv.DictReader(f)
68
+ if reader.fieldnames and not all(field in reader.fieldnames for field in ['full_path', 'file_summary', 'content_hash']):
69
+ raise ValueError("Missing required columns.")
70
+ for row in reader:
71
+ if 'full_path' in row and 'content_hash' in row:
72
+ # Use normalized path for cache key consistency
73
+ existing_data[os.path.normpath(row['full_path'])] = row
74
+ except Exception:
75
+ raise ValueError("Invalid CSV file format.")
140
76
 
141
- # Get all modification times at once to ensure consistent order
142
- file_mod_times = {f: os.path.getmtime(f) for f in files}
77
+ # Step 2: Load prompt template
78
+ prompt_template_name = "summarize_file_LLM"
79
+ prompt_template = load_prompt_template(prompt_template_name)
80
+ if not prompt_template:
81
+ raise FileNotFoundError(f"Prompt template '{prompt_template_name}' is empty or missing.")
143
82
 
144
- for file_path in track(files, description="Processing files..."):
145
- try:
146
- relative_path = os.path.relpath(file_path)
147
- normalized_path = normalize_path(relative_path)
148
- file_mod_time = file_mod_times[file_path]
149
- date_generated = datetime.now(UTC).isoformat()
83
+ # Step 3: Get list of files matching directory_path
84
+ # If directory_path is a directory, convert to recursive glob pattern
85
+ if os.path.isdir(directory_path):
86
+ search_pattern = os.path.join(directory_path, "**", "*")
87
+ else:
88
+ search_pattern = directory_path
150
89
 
90
+ files = glob.glob(search_pattern, recursive=True)
91
+
92
+ # Filter out directories, keep only files
93
+ # Also filter out __pycache__ and .pyc/.pyo files
94
+ filtered_files = []
95
+ for f in files:
96
+ if os.path.isfile(f):
97
+ if "__pycache__" in f:
98
+ continue
99
+ if f.endswith(('.pyc', '.pyo')):
100
+ continue
101
+ filtered_files.append(f)
102
+
103
+ files = filtered_files
104
+
105
+ # Step 4: Return early if no files
106
+ if not files:
107
+ # Return empty CSV header
108
+ output_io = io.StringIO()
109
+ writer = csv.DictWriter(output_io, fieldnames=['full_path', 'file_summary', 'content_hash'])
110
+ writer.writeheader()
111
+ return output_io.getvalue(), 0.0, "None"
112
+
113
+ results_data: List[Dict[str, str]] = []
114
+ total_cost = 0.0
115
+ last_model_name = "cached"
116
+
117
+ # Step 6: Iterate through files with progress reporting
118
+ total_files = len(files)
119
+
120
+ if progress_callback:
121
+ for i, file_path in enumerate(files):
122
+ progress_callback(i + 1, total_files)
123
+ cost, model = _process_single_file_logic(
124
+ file_path,
125
+ existing_data,
126
+ prompt_template,
127
+ strength,
128
+ temperature,
129
+ time,
130
+ verbose,
131
+ results_data
132
+ )
133
+ total_cost += cost
134
+ if model != "cached":
135
+ last_model_name = model
136
+ else:
137
+ console.print(f"[bold blue]Summarizing {len(files)} files in '{directory_path}'...[/bold blue]")
138
+ with Progress(
139
+ SpinnerColumn(),
140
+ TextColumn("[progress.description]{task.description}"),
141
+ BarColumn(),
142
+ TextColumn("{task.percentage:>3.0f}%"),
143
+ console=console
144
+ ) as progress:
145
+ task = progress.add_task("[cyan]Processing files...", total=len(files))
146
+ for file_path in files:
147
+ cost, model = _process_single_file_logic(
148
+ file_path,
149
+ existing_data,
150
+ prompt_template,
151
+ strength,
152
+ temperature,
153
+ time,
154
+ verbose,
155
+ results_data
156
+ )
157
+ total_cost += cost
158
+ if model != "cached":
159
+ last_model_name = model
160
+ progress.advance(task)
161
+
162
+ # Step 7: Generate CSV output
163
+ output_io = io.StringIO()
164
+ fieldnames = ['full_path', 'file_summary', 'content_hash']
165
+ writer = csv.DictWriter(output_io, fieldnames=fieldnames)
166
+
167
+ writer.writeheader()
168
+ writer.writerows(results_data)
169
+
170
+ csv_output = output_io.getvalue()
171
+
172
+ return csv_output, total_cost, last_model_name
173
+
174
+ def _process_single_file_logic(
175
+ file_path: str,
176
+ existing_data: Dict[str, Dict[str, str]],
177
+ prompt_template: str,
178
+ strength: float,
179
+ temperature: float,
180
+ time: float,
181
+ verbose: bool,
182
+ results_data: List[Dict[str, str]]
183
+ ) -> Tuple[float, str]:
184
+ """
185
+ Helper function to process a single file: read, hash, check cache, summarize if needed.
186
+ Returns (cost, model_name).
187
+ """
188
+ cost = 0.0
189
+ model_name = "cached"
190
+
191
+ try:
192
+ # Step 6a: Read file
193
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
194
+ content = f.read()
195
+
196
+ # Step 6b: Compute hash
197
+ current_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
198
+
199
+ summary = ""
200
+
201
+ # Step 6c: Check cache (using normalized path)
202
+ normalized_path = os.path.normpath(file_path)
203
+ cache_hit = False
204
+
205
+ if normalized_path in existing_data:
206
+ cached_entry = existing_data[normalized_path]
207
+ # Step 6d: Check hash match
208
+ if cached_entry.get('content_hash') == current_hash:
209
+ # Step 6e: Reuse summary
210
+ summary = cached_entry.get('file_summary', "")
211
+ cache_hit = True
151
212
  if verbose:
152
- print(f"\nProcessing file: {normalized_path}")
153
- print(f"Modification time: {datetime.fromtimestamp(file_mod_time, UTC)}")
154
-
155
- needs_summary = True
156
- if normalized_path in existing_data:
157
- try:
158
- existing_entry = existing_data[normalized_path]
159
- existing_date = parse_date(existing_entry['date'])
160
- file_date = datetime.fromtimestamp(file_mod_time, UTC)
161
-
162
- if verbose:
163
- print(f"Existing date: {existing_date}")
164
- print(f"File date: {file_date}")
165
-
166
- # Explicitly check if file is newer
167
- if file_date > existing_date:
168
- if verbose:
169
- print(f"[blue]File modified, generating new summary[/blue]")
170
- needs_summary = True
171
- else:
172
- needs_summary = False
173
- file_summary = existing_entry['file_summary']
174
- date_generated = existing_entry['date']
175
- if verbose:
176
- print(f"[green]Reusing existing summary[/green]")
177
- except Exception as e:
178
- if verbose:
179
- print(f"[yellow]Warning: Date comparison error: {str(e)}[/yellow]")
180
- needs_summary = True
181
- elif verbose:
182
- print("[blue]New file, generating summary[/blue]")
183
-
184
- if needs_summary:
185
- if verbose:
186
- print(f"[blue]Generating summary for: {normalized_path}[/blue]")
187
- with open(file_path, 'r', encoding='utf-8') as f:
188
- file_contents = f.read()
189
-
190
- input_params = {"file_contents": file_contents}
191
- response = llm_invoke(
192
- prompt=prompt_template,
193
- input_json=input_params,
194
- strength=strength,
195
- temperature=temperature,
196
- time=time,
197
- verbose=verbose,
198
- output_pydantic=FileSummary
199
- )
200
-
201
- if response.get('error'):
202
- file_summary = "Error in summarization."
203
- if verbose:
204
- print(f"[red]Error summarizing file: {response['error']}[/red]")
205
- else:
206
- file_summary = response['result'].file_summary
207
- total_cost += response.get('cost', 0.0)
208
- model_name = response.get('model_name', model_name)
209
-
210
- csv_output += f'"{relative_path}","{file_summary.replace(chr(34), "")}",{date_generated}\n'
211
-
212
- except Exception as e:
213
- if verbose:
214
- print(f"[red]Error processing file: {str(e)}[/red]")
215
- date_generated = datetime.now(UTC).isoformat()
216
- csv_output += f'"{relative_path}","Error processing file",{date_generated}\n'
213
+ console.print(f"[dim]Cache hit for {file_path}[/dim]")
217
214
 
218
- return csv_output, total_cost, model_name
215
+ # Step 6f: Summarize if needed
216
+ if not cache_hit:
217
+ if verbose:
218
+ console.print(f"[dim]Summarizing {file_path}...[/dim]")
219
+
220
+ llm_result = llm_invoke(
221
+ prompt=prompt_template,
222
+ input_json={"file_contents": content},
223
+ strength=strength,
224
+ temperature=temperature,
225
+ time=time,
226
+ output_pydantic=FileSummary,
227
+ verbose=verbose
228
+ )
229
+
230
+ file_summary_obj: FileSummary = llm_result['result']
231
+ summary = file_summary_obj.file_summary
232
+
233
+ cost = llm_result.get('cost', 0.0)
234
+ model_name = llm_result.get('model_name', "unknown")
235
+
236
+ # Step 6g: Store data
237
+ # Note: Requirement says "Store the relative path (not the full path)" in Step 6g description,
238
+ # but Output definition says "full_path". The existing code stored file_path (from glob).
239
+ # The new prompt Step 6g says "Store the relative path".
240
+ # However, the Output schema explicitly demands 'full_path'.
241
+ # To satisfy the Output schema which is usually the contract, we keep using file_path as 'full_path'.
242
+ # But we will calculate relative path if needed.
243
+ # Given the conflict, usually the Output definition takes precedence for the CSV column name,
244
+ # but the value might need to be relative.
245
+ # Let's stick to the existing behavior (glob path) which satisfied 'full_path' previously,
246
+ # unless 'relative path' implies os.path.relpath(file_path, start=directory_path_root).
247
+ # The prompt is slightly ambiguous: "Store the relative path... in the current data dictionary" vs Output "full_path".
248
+ # We will store the path as found by glob to ensure it matches the 'full_path' column expectation.
249
+
250
+ results_data.append({
251
+ 'full_path': file_path,
252
+ 'file_summary': summary,
253
+ 'content_hash': current_hash
254
+ })
219
255
 
220
256
  except Exception as e:
221
- print(f"[red]An error occurred: {str(e)}[/red]")
222
- raise
257
+ console.print(f"[bold red]Error processing file {file_path}:[/bold red] {e}")
258
+ results_data.append({
259
+ 'full_path': file_path,
260
+ 'file_summary': f"Error processing file: {str(e)}",
261
+ 'content_hash': "error"
262
+ })
263
+
264
+ return cost, model_name
pdd/sync_animation.py CHANGED
@@ -336,8 +336,13 @@ def _draw_connecting_lines_and_arrows(state: AnimationState, console_width: int)
336
336
  max_x = max(all_branch_xs)
337
337
 
338
338
  # Draw horizontal line on line 2 (index 2)
339
- for i in range(min_x, max_x + 1):
340
- line_parts[2][i] = "─"
339
+ # Clamp drawing range to console width to prevent IndexError and wrapping
340
+ draw_start = max(min_x, 0)
341
+ draw_end = min(max_x, console_width - 1)
342
+
343
+ if draw_start <= draw_end:
344
+ for i in range(draw_start, draw_end + 1):
345
+ line_parts[2][i] = "─"
341
346
 
342
347
  # Draw vertical connectors only where needed
343
348
  # Prompt always connects vertically (lines 0,1 above junction, lines 3,4,5 below)
@@ -639,5 +644,4 @@ def sync_animation(
639
644
  console.print_exception(show_locals=True)
640
645
  print(f"Error in animation: {e}", flush=True)
641
646
  finally:
642
- _final_logo_animation_sequence(console)
643
-
647
+ _final_logo_animation_sequence(console)