pdd-cli 0.0.39__py3-none-any.whl → 0.0.41__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.
Potentially problematic release.
This version of pdd-cli might be problematic. Click here for more details.
- pdd/__init__.py +5 -4
- pdd/auto_deps_main.py +10 -6
- pdd/auto_include.py +143 -101
- pdd/auto_update.py +76 -68
- pdd/bug_main.py +2 -2
- pdd/bug_to_unit_test.py +46 -38
- pdd/change.py +20 -13
- pdd/change_main.py +222 -162
- pdd/cli.py +111 -92
- pdd/cmd_test_main.py +51 -35
- pdd/crash_main.py +9 -8
- pdd/data/llm_model.csv +1 -1
- pdd/fix_verification_errors.py +13 -0
- pdd/fix_verification_main.py +2 -2
- pdd/get_extension.py +23 -9
- pdd/logo_animation.py +455 -0
- pdd/process_csv_change.py +1 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +2 -1
- pdd/prompts/sync_analysis_LLM.prompt +82 -0
- pdd/sync_animation.py +643 -0
- pdd/sync_determine_operation.py +574 -0
- pdd/xml_tagger.py +15 -6
- {pdd_cli-0.0.39.dist-info → pdd_cli-0.0.41.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.39.dist-info → pdd_cli-0.0.41.dist-info}/RECORD +28 -24
- {pdd_cli-0.0.39.dist-info → pdd_cli-0.0.41.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.39.dist-info → pdd_cli-0.0.41.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.39.dist-info → pdd_cli-0.0.41.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.39.dist-info → pdd_cli-0.0.41.dist-info}/top_level.txt +0 -0
pdd/get_extension.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
"""Module to retrieve file extensions for programming languages."""
|
|
2
2
|
|
|
3
|
-
# ```python
|
|
4
3
|
import os
|
|
5
4
|
import pandas as pd
|
|
6
5
|
|
|
7
|
-
def get_extension(language):
|
|
6
|
+
def get_extension(language: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Retrieves the file extension for a given programming language.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
language: The name of the programming language.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
The file extension (e.g., ".py") or an empty string if not found
|
|
15
|
+
or if the extension is invalid.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ValueError: If the PDD_PATH environment variable is not set.
|
|
19
|
+
FileNotFoundError: If the language_format.csv file is not found.
|
|
20
|
+
"""
|
|
8
21
|
# Step 1: Load the environment variable PDD_PATH
|
|
9
22
|
pdd_path = os.getenv('PDD_PATH')
|
|
10
23
|
if not pdd_path:
|
|
@@ -18,24 +31,25 @@ def get_extension(language):
|
|
|
18
31
|
|
|
19
32
|
# Step 3: Load the CSV file and look up the file extension
|
|
20
33
|
try:
|
|
21
|
-
|
|
22
|
-
except FileNotFoundError:
|
|
23
|
-
raise FileNotFoundError(
|
|
34
|
+
dataframe = pd.read_csv(csv_file_path)
|
|
35
|
+
except FileNotFoundError as exc:
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
f"The file {csv_file_path} does not exist."
|
|
38
|
+
) from exc
|
|
24
39
|
|
|
25
40
|
# Check if the language exists in the DataFrame
|
|
26
|
-
row =
|
|
41
|
+
row = dataframe[dataframe['language'].str.lower() == language_lower]
|
|
27
42
|
|
|
28
43
|
# Step 4: Return the file extension or an empty string if not found
|
|
29
44
|
if not row.empty:
|
|
30
45
|
extension = row['extension'].values[0]
|
|
31
46
|
return extension if isinstance(extension, str) and extension else ''
|
|
32
|
-
|
|
47
|
+
|
|
33
48
|
return ''
|
|
34
49
|
|
|
35
50
|
# Example usage:
|
|
36
51
|
# Assuming the environment variable PDD_PATH is set correctly
|
|
37
52
|
# print(get_extension('Python')) # Output: .py
|
|
38
|
-
# ```
|
|
39
53
|
|
|
40
54
|
# ### Explanation of the Code:
|
|
41
55
|
# 1. **Environment Variable**: We use `os.getenv` to retrieve the `PDD_PATH` environment variable. If it's not set, we raise a `ValueError`.
|
pdd/logo_animation.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# pdd/logo_animation.py
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
import math
|
|
5
|
+
from typing import List, Tuple, Optional
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.live import Live
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from rich.style import Style
|
|
12
|
+
|
|
13
|
+
# Attempt to import constants from the package structure
|
|
14
|
+
# These will be mocked in the __main__ block for direct execution testing
|
|
15
|
+
try:
|
|
16
|
+
from . import (
|
|
17
|
+
ELECTRIC_CYAN, DEEP_NAVY,
|
|
18
|
+
LOGO_FORMATION_DURATION, LOGO_TO_BOX_TRANSITION_DURATION,
|
|
19
|
+
EXPANDED_BOX_HEIGHT, ANIMATION_FRAME_RATE, ASCII_LOGO_ART,
|
|
20
|
+
DEFAULT_TIME as LOGO_HOLD_DURATION # Use DEFAULT_TIME for hold duration
|
|
21
|
+
)
|
|
22
|
+
except ImportError:
|
|
23
|
+
# Fallback for direct execution or if constants are not yet in __init__.py
|
|
24
|
+
# This section will be overridden by __main__ for testing
|
|
25
|
+
ELECTRIC_CYAN = "#00D8FF"
|
|
26
|
+
DEEP_NAVY = "#0A0A23"
|
|
27
|
+
LOGO_FORMATION_DURATION = 1.5
|
|
28
|
+
LOGO_HOLD_DURATION = 1.0
|
|
29
|
+
LOGO_TO_BOX_TRANSITION_DURATION = 1.5
|
|
30
|
+
EXPANDED_BOX_HEIGHT = 18
|
|
31
|
+
ANIMATION_FRAME_RATE = 20
|
|
32
|
+
ASCII_LOGO_ART = """
|
|
33
|
+
+xxxxxxxxxxxxxxx+
|
|
34
|
+
xxxxxxxxxxxxxxxxxxxxx+
|
|
35
|
+
xxx +xx+
|
|
36
|
+
xxx x+ xx+
|
|
37
|
+
xxx x+ xxx
|
|
38
|
+
xxx x+ xx+
|
|
39
|
+
xxx x+ xx+
|
|
40
|
+
xxx x+ xxx
|
|
41
|
+
xxx +xx+
|
|
42
|
+
xxx +xxxxxxxxxxx+
|
|
43
|
+
xxx +xx+
|
|
44
|
+
xxx +xx+
|
|
45
|
+
xxx+xx+
|
|
46
|
+
xxxx+
|
|
47
|
+
xx+
|
|
48
|
+
""".strip().splitlines()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class AnimatedParticle:
|
|
53
|
+
"""Represents a single character in the animated logo."""
|
|
54
|
+
char: str
|
|
55
|
+
orig_logo_x: int # Original relative X in ASCII_LOGO_ART
|
|
56
|
+
orig_logo_y: int # Original relative Y in ASCII_LOGO_ART
|
|
57
|
+
|
|
58
|
+
start_x: float = 0.0
|
|
59
|
+
start_y: float = 0.0
|
|
60
|
+
current_x: float = 0.0
|
|
61
|
+
current_y: float = 0.0
|
|
62
|
+
target_x: float = 0.0
|
|
63
|
+
target_y: float = 0.0
|
|
64
|
+
|
|
65
|
+
style: Style = field(default_factory=lambda: Style(color=ELECTRIC_CYAN))
|
|
66
|
+
visible: bool = True
|
|
67
|
+
|
|
68
|
+
def update_progress(self, progress: float):
|
|
69
|
+
"""Updates current_x, current_y based on linear interpolation."""
|
|
70
|
+
self.current_x = self.start_x + (self.target_x - self.start_x) * progress
|
|
71
|
+
self.current_y = self.start_y + (self.target_y - self.start_y) * progress
|
|
72
|
+
|
|
73
|
+
def set_new_transition(self, new_target_x: float, new_target_y: float):
|
|
74
|
+
"""Sets the current position as the start for a new transition."""
|
|
75
|
+
self.start_x = self.current_x
|
|
76
|
+
self.start_y = self.current_y
|
|
77
|
+
self.target_x = new_target_x
|
|
78
|
+
self.target_y = new_target_y
|
|
79
|
+
|
|
80
|
+
_stop_animation_event = threading.Event()
|
|
81
|
+
_animation_thread: Optional[threading.Thread] = None
|
|
82
|
+
|
|
83
|
+
def _parse_logo_art(logo_art_lines: Optional[List[str]]) -> List[AnimatedParticle]:
|
|
84
|
+
"""Converts ASCII art strings into a list of AnimatedParticle objects."""
|
|
85
|
+
if logo_art_lines is None: # Handle None input gracefully
|
|
86
|
+
return []
|
|
87
|
+
particles: List[AnimatedParticle] = []
|
|
88
|
+
for y, line in enumerate(logo_art_lines):
|
|
89
|
+
for x, char_val in enumerate(line):
|
|
90
|
+
if char_val != ' ': # Only animate non-space characters
|
|
91
|
+
particles.append(AnimatedParticle(char=char_val, orig_logo_x=x, orig_logo_y=y))
|
|
92
|
+
return particles
|
|
93
|
+
|
|
94
|
+
def _get_centered_logo_positions(
|
|
95
|
+
particles: List[AnimatedParticle],
|
|
96
|
+
logo_art_lines: List[str], # Assumes logo_art_lines is not None here due to checks before calling
|
|
97
|
+
console_width: int,
|
|
98
|
+
console_height: int
|
|
99
|
+
) -> List[Tuple[int, int]]:
|
|
100
|
+
"""Calculates target positions for particles to form the centered logo."""
|
|
101
|
+
if not logo_art_lines: return [(0,0)] * len(particles) # Should not happen if particles exist
|
|
102
|
+
logo_width = max(len(line) for line in logo_art_lines) if logo_art_lines else 0
|
|
103
|
+
logo_height = len(logo_art_lines)
|
|
104
|
+
|
|
105
|
+
offset_x = (console_width - logo_width) // 2
|
|
106
|
+
offset_y = (console_height - logo_height) // 2
|
|
107
|
+
|
|
108
|
+
target_positions: List[Tuple[int,int]] = []
|
|
109
|
+
for p in particles:
|
|
110
|
+
target_positions.append((p.orig_logo_x + offset_x, p.orig_logo_y + offset_y))
|
|
111
|
+
return target_positions
|
|
112
|
+
|
|
113
|
+
def _get_box_perimeter_positions(
|
|
114
|
+
particles: List[AnimatedParticle],
|
|
115
|
+
console_width: int,
|
|
116
|
+
console_height: int
|
|
117
|
+
) -> List[Tuple[int, int]]:
|
|
118
|
+
"""Calculates target positions for particles on the perimeter of an expanded box."""
|
|
119
|
+
actual_box_height = min(EXPANDED_BOX_HEIGHT, console_height)
|
|
120
|
+
actual_box_width = max(1, console_width) # Ensure width is at least 1
|
|
121
|
+
|
|
122
|
+
box_start_y = (console_height - actual_box_height) // 2
|
|
123
|
+
box_start_x = 0
|
|
124
|
+
|
|
125
|
+
perimeter_points: List[Tuple[int, int]] = []
|
|
126
|
+
# Top edge
|
|
127
|
+
for x in range(actual_box_width):
|
|
128
|
+
perimeter_points.append((box_start_x + x, box_start_y))
|
|
129
|
+
# Right edge (excluding corners if covered)
|
|
130
|
+
if actual_box_height > 1:
|
|
131
|
+
for y in range(1, actual_box_height - 1):
|
|
132
|
+
perimeter_points.append((box_start_x + actual_box_width - 1, box_start_y + y))
|
|
133
|
+
# Bottom edge (including corners if not covered)
|
|
134
|
+
if actual_box_height > 1:
|
|
135
|
+
for x in range(actual_box_width - 1, -1, -1):
|
|
136
|
+
perimeter_points.append((box_start_x + x, box_start_y + actual_box_height - 1))
|
|
137
|
+
# Left edge (excluding corners if covered)
|
|
138
|
+
if actual_box_width > 1 and actual_box_height > 2:
|
|
139
|
+
for y in range(actual_box_height - 2, 0, -1):
|
|
140
|
+
perimeter_points.append((box_start_x, box_start_y + y))
|
|
141
|
+
|
|
142
|
+
if not perimeter_points: # Fallback for very small console
|
|
143
|
+
perimeter_points.append((box_start_x, box_start_y))
|
|
144
|
+
|
|
145
|
+
num_particles = len(particles)
|
|
146
|
+
target_positions: List[Tuple[int,int]] = []
|
|
147
|
+
if not num_particles: return []
|
|
148
|
+
|
|
149
|
+
for i in range(num_particles):
|
|
150
|
+
# Distribute particles along the perimeter
|
|
151
|
+
idx = math.floor(i * (len(perimeter_points) / num_particles))
|
|
152
|
+
target_positions.append(perimeter_points[idx % len(perimeter_points)])
|
|
153
|
+
return target_positions
|
|
154
|
+
|
|
155
|
+
def _render_particles_to_text(
|
|
156
|
+
particles: List[AnimatedParticle],
|
|
157
|
+
console_width: int,
|
|
158
|
+
console_height: int = 18 # This argument is console_height for rendering logic
|
|
159
|
+
) -> Text:
|
|
160
|
+
"""Renders particles onto a Rich Text object for display with fixed 18-line height."""
|
|
161
|
+
# Use fixed height to match sync_animation.py and prompt requirement
|
|
162
|
+
fixed_render_height = 18 # Explicitly use 18 for rendering grid
|
|
163
|
+
|
|
164
|
+
# Initialize Text with background color for fixed height
|
|
165
|
+
text = Text(style=Style(bgcolor=DEEP_NAVY))
|
|
166
|
+
|
|
167
|
+
# Create a 2D grid for characters and their styles
|
|
168
|
+
char_grid = [[' ' for _ in range(console_width)] for _ in range(fixed_render_height)]
|
|
169
|
+
# Base style for empty cells (background color, foreground matches background to be "invisible")
|
|
170
|
+
base_style = Style(color=DEEP_NAVY, bgcolor=DEEP_NAVY)
|
|
171
|
+
style_map = [[base_style for _ in range(console_width)] for _ in range(fixed_render_height)]
|
|
172
|
+
|
|
173
|
+
# Style for particles (foreground color, global background)
|
|
174
|
+
particle_render_style = Style(bgcolor=DEEP_NAVY)
|
|
175
|
+
|
|
176
|
+
# Place particles onto the grid
|
|
177
|
+
for p in particles:
|
|
178
|
+
if p.visible:
|
|
179
|
+
x, y = int(round(p.current_x)), int(round(p.current_y))
|
|
180
|
+
if 0 <= y < fixed_render_height and 0 <= x < console_width:
|
|
181
|
+
char_grid[y][x] = p.char
|
|
182
|
+
style_map[y][x] = p.style + particle_render_style
|
|
183
|
+
|
|
184
|
+
# Assemble the Text object row by row, optimizing for style runs
|
|
185
|
+
for r_idx in range(fixed_render_height):
|
|
186
|
+
current_run_chars: List[str] = []
|
|
187
|
+
current_run_style: Optional[Style] = None
|
|
188
|
+
|
|
189
|
+
for c_idx in range(console_width):
|
|
190
|
+
char_val = char_grid[r_idx][c_idx]
|
|
191
|
+
style_val = style_map[r_idx][c_idx]
|
|
192
|
+
|
|
193
|
+
if current_run_style is None: # Start of a new run
|
|
194
|
+
current_run_chars.append(char_val)
|
|
195
|
+
current_run_style = style_val
|
|
196
|
+
elif style_val == current_run_style: # Continue current run
|
|
197
|
+
current_run_chars.append(char_val)
|
|
198
|
+
else: # Style changed, finalize previous run and start new
|
|
199
|
+
if current_run_style is not None: # Should always be true here
|
|
200
|
+
text.append("".join(current_run_chars), current_run_style)
|
|
201
|
+
current_run_chars = [char_val]
|
|
202
|
+
current_run_style = style_val
|
|
203
|
+
|
|
204
|
+
# Append any remaining run from the row
|
|
205
|
+
if current_run_chars and current_run_style is not None:
|
|
206
|
+
text.append("".join(current_run_chars), current_run_style)
|
|
207
|
+
|
|
208
|
+
if r_idx < fixed_render_height - 1:
|
|
209
|
+
text.append("\n") # Add newline between rows
|
|
210
|
+
|
|
211
|
+
return text
|
|
212
|
+
|
|
213
|
+
def _animation_loop(console: Console):
|
|
214
|
+
"""Main loop for the animation, running in a separate thread."""
|
|
215
|
+
effective_frame_rate = max(1, ANIMATION_FRAME_RATE) # Ensure positive frame rate
|
|
216
|
+
frame_duration = 1.0 / effective_frame_rate
|
|
217
|
+
|
|
218
|
+
# Ensure ASCII_LOGO_ART is a list of strings, or handle if it's None (via _parse_logo_art)
|
|
219
|
+
local_ascii_logo_art: Optional[List[str]] = ASCII_LOGO_ART
|
|
220
|
+
if isinstance(local_ascii_logo_art, str):
|
|
221
|
+
local_ascii_logo_art = local_ascii_logo_art.strip().splitlines()
|
|
222
|
+
|
|
223
|
+
particles = _parse_logo_art(local_ascii_logo_art)
|
|
224
|
+
if not particles: return # No particles to animate (handles None or empty art)
|
|
225
|
+
|
|
226
|
+
# All subsequent uses of local_ascii_logo_art can assume it's List[str]
|
|
227
|
+
# because if it were None, `particles` would be empty and we'd have returned.
|
|
228
|
+
# However, to satisfy type checkers if they can't infer this, an explicit assertion or check
|
|
229
|
+
# might be needed if local_ascii_logo_art is directly used later and needs to be List[str].
|
|
230
|
+
# For _get_centered_logo_positions, it's passed, and that function expects List[str].
|
|
231
|
+
# Let's ensure it's not None before passing if type hints are strict.
|
|
232
|
+
# Given the logic, if particles is not empty, local_ascii_logo_art must have been a non-empty List[str].
|
|
233
|
+
# If local_ascii_logo_art was None, particles is [], loop returns.
|
|
234
|
+
# If local_ascii_logo_art was [], particles is [], loop returns.
|
|
235
|
+
# So, if we reach here, local_ascii_logo_art must be a non-empty List[str].
|
|
236
|
+
|
|
237
|
+
# The console height for animation logic is fixed at 18 lines as per prompt.
|
|
238
|
+
animation_console_height = 18
|
|
239
|
+
console_width = console.width # Use actual console width
|
|
240
|
+
|
|
241
|
+
# Set initial style for particles (foreground color)
|
|
242
|
+
for p in particles:
|
|
243
|
+
p.style = Style(color=ELECTRIC_CYAN)
|
|
244
|
+
|
|
245
|
+
# Stage 1: Formation - Particles travel from bottom-left to form logo
|
|
246
|
+
# We know local_ascii_logo_art is List[str] here if particles is not empty.
|
|
247
|
+
logo_target_positions = _get_centered_logo_positions(particles, local_ascii_logo_art, console_width, animation_console_height)
|
|
248
|
+
for i, p in enumerate(particles):
|
|
249
|
+
p.start_x = 0.0 # Start at bottom-left
|
|
250
|
+
p.start_y = float(animation_console_height - 1)
|
|
251
|
+
p.current_x, p.current_y = p.start_x, p.start_y
|
|
252
|
+
p.target_x, p.target_y = float(logo_target_positions[i][0]), float(logo_target_positions[i][1])
|
|
253
|
+
|
|
254
|
+
with Live(console=console, refresh_per_second=effective_frame_rate, transient=True, screen=True) as live:
|
|
255
|
+
# Animation Stage 1: Formation
|
|
256
|
+
stage_duration = LOGO_FORMATION_DURATION or 0.1 # Ensure non-zero
|
|
257
|
+
stage_start_time = time.monotonic()
|
|
258
|
+
while not _stop_animation_event.is_set():
|
|
259
|
+
elapsed = time.monotonic() - stage_start_time
|
|
260
|
+
progress = min(elapsed / stage_duration, 1.0) if stage_duration > 0 else 1.0
|
|
261
|
+
|
|
262
|
+
for p_obj in particles: p_obj.update_progress(progress)
|
|
263
|
+
live.update(_render_particles_to_text(particles, console_width, animation_console_height))
|
|
264
|
+
|
|
265
|
+
if progress >= 1.0: break
|
|
266
|
+
if _stop_animation_event.wait(frame_duration): break
|
|
267
|
+
|
|
268
|
+
if _stop_animation_event.is_set(): return
|
|
269
|
+
|
|
270
|
+
# Hold Stage: Display formed logo
|
|
271
|
+
hold_duration = LOGO_HOLD_DURATION or 0.1 # Ensure non-zero
|
|
272
|
+
hold_start_time = time.monotonic()
|
|
273
|
+
while not _stop_animation_event.is_set():
|
|
274
|
+
if time.monotonic() - hold_start_time >= hold_duration: break
|
|
275
|
+
live.update(_render_particles_to_text(particles, console_width, animation_console_height)) # Keep rendering
|
|
276
|
+
if _stop_animation_event.wait(frame_duration): break
|
|
277
|
+
|
|
278
|
+
if _stop_animation_event.is_set(): return
|
|
279
|
+
|
|
280
|
+
# Animation Stage 2: Expansion to Box
|
|
281
|
+
# _get_box_perimeter_positions uses console.height (from the console object) for its logic,
|
|
282
|
+
# but the prompt specifies the box should be 18 lines tall.
|
|
283
|
+
# The EXPANDED_BOX_HEIGHT constant is 18.
|
|
284
|
+
# _get_box_perimeter_positions uses min(EXPANDED_BOX_HEIGHT, console_height_arg)
|
|
285
|
+
# We should pass animation_console_height (18) to ensure the box logic aims for 18 lines.
|
|
286
|
+
box_target_positions = _get_box_perimeter_positions(particles, console_width, animation_console_height)
|
|
287
|
+
for i, p_obj in enumerate(particles):
|
|
288
|
+
p_obj.set_new_transition(float(box_target_positions[i][0]), float(box_target_positions[i][1]))
|
|
289
|
+
|
|
290
|
+
stage_duration = LOGO_TO_BOX_TRANSITION_DURATION or 0.1 # Ensure non-zero
|
|
291
|
+
stage_start_time = time.monotonic()
|
|
292
|
+
while not _stop_animation_event.is_set():
|
|
293
|
+
elapsed = time.monotonic() - stage_start_time
|
|
294
|
+
progress = min(elapsed / stage_duration, 1.0) if stage_duration > 0 else 1.0
|
|
295
|
+
|
|
296
|
+
for p_obj in particles: p_obj.update_progress(progress)
|
|
297
|
+
live.update(_render_particles_to_text(particles, console_width, animation_console_height))
|
|
298
|
+
|
|
299
|
+
if progress >= 1.0: break
|
|
300
|
+
if _stop_animation_event.wait(frame_duration): break
|
|
301
|
+
|
|
302
|
+
# By removing the final 'while' loop that was here, the 'with Live(...)'
|
|
303
|
+
# context will now properly exit, allowing other terminal output to be displayed.
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def start_logo_animation():
|
|
307
|
+
"""Starts the logo animation in a separate daemon thread."""
|
|
308
|
+
global _animation_thread
|
|
309
|
+
if _animation_thread and _animation_thread.is_alive():
|
|
310
|
+
return # Animation already running
|
|
311
|
+
|
|
312
|
+
_stop_animation_event.clear()
|
|
313
|
+
# The Console instance created here will have its own width/height.
|
|
314
|
+
# _animation_loop uses console.width but hardcodes its internal animation_console_height to 18.
|
|
315
|
+
console = Console(color_system="truecolor")
|
|
316
|
+
|
|
317
|
+
_animation_thread = threading.Thread(target=_animation_loop, args=(console,), daemon=True)
|
|
318
|
+
_animation_thread.start()
|
|
319
|
+
|
|
320
|
+
def stop_logo_animation():
|
|
321
|
+
"""Signals the animation thread to stop and waits for it to terminate."""
|
|
322
|
+
global _animation_thread
|
|
323
|
+
_stop_animation_event.set()
|
|
324
|
+
if _animation_thread and _animation_thread.is_alive():
|
|
325
|
+
# Calculate a reasonable join timeout based on animation durations
|
|
326
|
+
# Use max(0.1, ...) for durations to avoid issues if they are 0 or None
|
|
327
|
+
timeout = (max(0.1, LOGO_FORMATION_DURATION or 0.1) +
|
|
328
|
+
max(0.1, LOGO_HOLD_DURATION or 0.1) +
|
|
329
|
+
max(0.1, LOGO_TO_BOX_TRANSITION_DURATION or 0.1) +
|
|
330
|
+
2.0) # Add a buffer
|
|
331
|
+
_animation_thread.join(timeout=max(0.1, timeout)) # Ensure timeout is positive
|
|
332
|
+
_animation_thread = None
|
|
333
|
+
|
|
334
|
+
# run_logo_animation_inline is not part of the primary API being tested by unit tests,
|
|
335
|
+
# but for completeness, ensure its logic for console_height is consistent if it were used.
|
|
336
|
+
# It currently uses `console_height = 18` which is consistent.
|
|
337
|
+
def run_logo_animation_inline(console: Console, stop_event: threading.Event):
|
|
338
|
+
"""Runs the logo animation inline without screen=True to avoid conflicts."""
|
|
339
|
+
effective_frame_rate = max(1, ANIMATION_FRAME_RATE)
|
|
340
|
+
frame_duration = 1.0 / effective_frame_rate
|
|
341
|
+
|
|
342
|
+
local_ascii_logo_art: Optional[List[str]] = ASCII_LOGO_ART
|
|
343
|
+
if isinstance(local_ascii_logo_art, str):
|
|
344
|
+
local_ascii_logo_art = local_ascii_logo_art.strip().splitlines()
|
|
345
|
+
|
|
346
|
+
particles = _parse_logo_art(local_ascii_logo_art)
|
|
347
|
+
if not particles:
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
animation_console_height = 18 # Fixed height for animation logic
|
|
351
|
+
console_width = console.width
|
|
352
|
+
|
|
353
|
+
for p in particles:
|
|
354
|
+
p.style = Style(color=ELECTRIC_CYAN)
|
|
355
|
+
|
|
356
|
+
logo_target_positions = _get_centered_logo_positions(particles, local_ascii_logo_art, console_width, animation_console_height)
|
|
357
|
+
for i, p in enumerate(particles):
|
|
358
|
+
p.start_x = 0.0
|
|
359
|
+
p.start_y = float(animation_console_height - 1)
|
|
360
|
+
p.current_x, p.current_y = p.start_x, p.start_y
|
|
361
|
+
p.target_x, p.target_y = float(logo_target_positions[i][0]), float(logo_target_positions[i][1])
|
|
362
|
+
|
|
363
|
+
with Live(console=console, refresh_per_second=effective_frame_rate, transient=True, screen=True) as live:
|
|
364
|
+
stage_duration = LOGO_FORMATION_DURATION or 0.1
|
|
365
|
+
stage_start_time = time.monotonic()
|
|
366
|
+
while not stop_event.is_set():
|
|
367
|
+
elapsed = time.monotonic() - stage_start_time
|
|
368
|
+
progress = min(elapsed / stage_duration, 1.0) if stage_duration > 0 else 1.0
|
|
369
|
+
for p_obj in particles: p_obj.update_progress(progress)
|
|
370
|
+
live.update(_render_particles_to_text(particles, console_width, animation_console_height))
|
|
371
|
+
if progress >= 1.0: break
|
|
372
|
+
if stop_event.wait(frame_duration): break
|
|
373
|
+
|
|
374
|
+
if stop_event.is_set(): return
|
|
375
|
+
|
|
376
|
+
hold_duration = LOGO_HOLD_DURATION or 0.1
|
|
377
|
+
hold_start_time = time.monotonic()
|
|
378
|
+
while not stop_event.is_set():
|
|
379
|
+
if time.monotonic() - hold_start_time >= hold_duration: break
|
|
380
|
+
live.update(_render_particles_to_text(particles, console_width, animation_console_height))
|
|
381
|
+
if stop_event.wait(frame_duration): break
|
|
382
|
+
|
|
383
|
+
if stop_event.is_set(): return
|
|
384
|
+
|
|
385
|
+
box_target_positions = _get_box_perimeter_positions(particles, console_width, animation_console_height)
|
|
386
|
+
for i, p_obj in enumerate(particles):
|
|
387
|
+
p_obj.set_new_transition(float(box_target_positions[i][0]), float(box_target_positions[i][1]))
|
|
388
|
+
|
|
389
|
+
stage_duration = LOGO_TO_BOX_TRANSITION_DURATION or 0.1
|
|
390
|
+
stage_start_time = time.monotonic()
|
|
391
|
+
while not stop_event.is_set():
|
|
392
|
+
elapsed = time.monotonic() - stage_start_time
|
|
393
|
+
progress = min(elapsed / stage_duration, 1.0) if stage_duration > 0 else 1.0
|
|
394
|
+
for p_obj in particles: p_obj.update_progress(progress)
|
|
395
|
+
live.update(_render_particles_to_text(particles, console_width, animation_console_height))
|
|
396
|
+
if progress >= 1.0: break
|
|
397
|
+
if stop_event.wait(frame_duration): break
|
|
398
|
+
|
|
399
|
+
# By removing the final 'while' loop that was here, the 'with Live(...)'
|
|
400
|
+
# context will now properly exit.
|
|
401
|
+
|
|
402
|
+
# Main block for testing the animation directly
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
# Mock constants for direct testing if they weren't imported (e.g. running file directly)
|
|
405
|
+
mock_constants = {
|
|
406
|
+
"ELECTRIC_CYAN": "#00D8FF", "DEEP_NAVY": "#0A0A23",
|
|
407
|
+
"LOGO_FORMATION_DURATION": 1.5, "LOGO_HOLD_DURATION": 1.0,
|
|
408
|
+
"LOGO_TO_BOX_TRANSITION_DURATION": 1.5, "EXPANDED_BOX_HEIGHT": 18,
|
|
409
|
+
"ANIMATION_FRAME_RATE": 20,
|
|
410
|
+
"ASCII_LOGO_ART": """
|
|
411
|
+
+xxxxxxxxxxxxxxx+
|
|
412
|
+
xxxxxxxxxxxxxxxxxxxxx+
|
|
413
|
+
xxx +xx+
|
|
414
|
+
xxx x+ xx+
|
|
415
|
+
xxx x+ xxx
|
|
416
|
+
xxx x+ xx+
|
|
417
|
+
xxx x+ xx+
|
|
418
|
+
xxx x+ xxx
|
|
419
|
+
xxx +xx+
|
|
420
|
+
xxx +xxxxxxxxxxx+
|
|
421
|
+
xxx +xx+
|
|
422
|
+
xxx +xx+
|
|
423
|
+
xxx+xx+
|
|
424
|
+
xxxx+
|
|
425
|
+
xx+
|
|
426
|
+
""".strip().splitlines()
|
|
427
|
+
}
|
|
428
|
+
# Apply mocks if constants are not defined or are None (e.g., due to failed import)
|
|
429
|
+
for const_name, const_val in mock_constants.items():
|
|
430
|
+
if const_name not in globals() or globals()[const_name] is None:
|
|
431
|
+
globals()[const_name] = const_val
|
|
432
|
+
|
|
433
|
+
# Special handling for LOGO_HOLD_DURATION if DEFAULT_TIME logic was intended
|
|
434
|
+
if 'LOGO_HOLD_DURATION' not in globals() or globals()['LOGO_HOLD_DURATION'] is None:
|
|
435
|
+
# If 'DEFAULT_TIME' was meant to be imported as LOGO_HOLD_DURATION and failed
|
|
436
|
+
globals()['LOGO_HOLD_DURATION'] = mock_constants['LOGO_HOLD_DURATION']
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
print("Starting logo animation test (Press Ctrl+C to stop)...")
|
|
440
|
+
print(f" Formation: {globals()['LOGO_FORMATION_DURATION']}s, Hold: {globals()['LOGO_HOLD_DURATION']}s, Expansion: {globals()['LOGO_TO_BOX_TRANSITION_DURATION']}s")
|
|
441
|
+
|
|
442
|
+
start_logo_animation()
|
|
443
|
+
try:
|
|
444
|
+
# Calculate total expected animation time for the sleep duration
|
|
445
|
+
total_anim_duration = (globals().get('LOGO_FORMATION_DURATION', 0) or 0) + \
|
|
446
|
+
(globals().get('LOGO_HOLD_DURATION', 0) or 0) + \
|
|
447
|
+
(globals().get('LOGO_TO_BOX_TRANSITION_DURATION', 0) or 0)
|
|
448
|
+
# Sleep a bit longer than the animation to see its full course
|
|
449
|
+
time.sleep(max(0.1, total_anim_duration + 2.0))
|
|
450
|
+
except KeyboardInterrupt:
|
|
451
|
+
print("\nInterrupted by user.")
|
|
452
|
+
finally:
|
|
453
|
+
print("Stopping logo animation...")
|
|
454
|
+
stop_logo_animation()
|
|
455
|
+
print("Animation stopped.")
|
pdd/process_csv_change.py
CHANGED
|
@@ -302,7 +302,7 @@ def process_csv_change(
|
|
|
302
302
|
temperature=temperature,
|
|
303
303
|
time=time, # Pass time
|
|
304
304
|
budget=budget - total_cost, # Pass per-row budget
|
|
305
|
-
|
|
305
|
+
# verbose=verbose Suppress individual change prints for CSV mode
|
|
306
306
|
)
|
|
307
307
|
console.print(f" [dim]Change cost:[/dim] ${cost:.6f}")
|
|
308
308
|
console.print(f" [dim]Model used:[/dim] {current_model_name}")
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
% Here is the original code module: <code_module>{code}</code_module>
|
|
6
6
|
|
|
7
|
-
% Here is the program/code module bug fix report:
|
|
7
|
+
% Here is the program/code module bug fix report: <program_code_fix>
|
|
8
|
+
{program_code_fix}</program_code_fix>
|
|
8
9
|
|
|
9
10
|
% Sometimes the fix may only contain partial code. In these cases, you need to incorporate the fix into the original program and/or original code module.
|
|
10
11
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# sync_analysis_LLM.prompt
|
|
2
|
+
|
|
3
|
+
You are an expert PDD (Prompt-Driven Development) sync analyzer. Your task is to act as an automated code reviewer and merge strategist. You will be given the last known good state of a PDD unit (the "fingerprint") and the `diff` for each file that has changed.
|
|
4
|
+
|
|
5
|
+
Analyze the changes and provide a precise, actionable strategy in JSON format for reconciling the modifications.
|
|
6
|
+
|
|
7
|
+
### Last Known Good State (Fingerprint)
|
|
8
|
+
This is the set of file hashes from the last successful PDD operation.
|
|
9
|
+
<fingerprint>
|
|
10
|
+
{fingerprint}
|
|
11
|
+
</fingerprint>
|
|
12
|
+
|
|
13
|
+
### Changed Files
|
|
14
|
+
The following files have changed since the last sync: {changed_files_list}
|
|
15
|
+
|
|
16
|
+
### File Diffs
|
|
17
|
+
Here are the `git diff` outputs showing the changes for each file against the last committed version.
|
|
18
|
+
|
|
19
|
+
#### Prompt Diff (`{prompt_path}`):
|
|
20
|
+
<prompt_diff>
|
|
21
|
+
{prompt_diff}
|
|
22
|
+
</prompt_diff>
|
|
23
|
+
|
|
24
|
+
#### Code Diff (`{code_path}`):
|
|
25
|
+
<code_diff>
|
|
26
|
+
{code_diff}
|
|
27
|
+
</code_diff>
|
|
28
|
+
|
|
29
|
+
#### Example Diff (`{example_path}`):
|
|
30
|
+
<example_diff>
|
|
31
|
+
{example_diff}
|
|
32
|
+
</example_diff>
|
|
33
|
+
|
|
34
|
+
#### Test Diff (`{test_path}`):
|
|
35
|
+
<test_diff>
|
|
36
|
+
{test_diff}
|
|
37
|
+
</test_diff>
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Analysis Task
|
|
42
|
+
|
|
43
|
+
Based on the fingerprint and the provided diffs, determine the most logical and safe way to proceed.
|
|
44
|
+
|
|
45
|
+
### Key Principles for Your Decision:
|
|
46
|
+
|
|
47
|
+
1. **Preserve Intent**: The top priority is to avoid losing user work. Manual bug fixes in code or new user-written tests are valuable.
|
|
48
|
+
2. **Prompt is the Goal**: Changes in the prompt usually represent the desired future state of the code.
|
|
49
|
+
3. **Code is the Reality**: Manual changes in the code often represent fixes or improvements that the prompt doesn't know about yet.
|
|
50
|
+
4. **Analyze the *Nature* of Changes**:
|
|
51
|
+
- Is the prompt change a new feature or just a clarification?
|
|
52
|
+
- Is the code change a small bug fix or a major refactoring?
|
|
53
|
+
- Do the changes conflict? (e.g., prompt wants to remove a function that was just manually fixed in the code).
|
|
54
|
+
|
|
55
|
+
## Response Format
|
|
56
|
+
|
|
57
|
+
Respond **only** with a single JSON object. Do not add any explanatory text before or after the JSON block.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"next_operation": "generate|update|fix|test|verify|fail_and_request_manual_merge",
|
|
62
|
+
"reason": "A clear, concise explanation of the situation and the rationale for your chosen operation.",
|
|
63
|
+
"merge_strategy": {
|
|
64
|
+
"type": "preserve_code_and_regenerate|update_prompt_from_code|three_way_merge_safe|three_way_merge_unsafe|none",
|
|
65
|
+
"description": "A human-readable description of the merge plan.",
|
|
66
|
+
"preservation_notes": [
|
|
67
|
+
"A list of specific, actionable notes for the merge process. For example: 'Preserve the body of the `calculate_total` function in the code file.' or 'Merge the new tests from the user, then regenerate the rest of the test file.'"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"confidence": 0.9,
|
|
71
|
+
"follow_up_operations": ["A list of likely PDD operations to run after this one succeeds (e.g., 'test', 'verify')."]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `merge_strategy.type` Definitions:
|
|
76
|
+
|
|
77
|
+
- **`preserve_code_and_regenerate`**: Use when the prompt has significant new features, but the code contains manual changes that should be preserved. The `next_operation` should be `generate`.
|
|
78
|
+
- **`update_prompt_from_code`**: Use when the code has been significantly refactored or changed in a way that makes the prompt outdated. The `next_operation` should be `update`.
|
|
79
|
+
- **`three_way_merge_safe`**: Use when changes are in different, non-conflicting parts of the files. The system can likely merge them automatically. The `next_operation` could be `generate` or `update`.
|
|
80
|
+
- **`three_way_merge_unsafe`**: Use when changes conflict directly and an automated merge is risky. This is a suggestion to proceed with caution.
|
|
81
|
+
- **`none`**: Use when the `next_operation` doesn't involve a merge (e.g., `test` or `fix`).
|
|
82
|
+
- **`fail_and_request_manual_merge`**: Use when the conflict is too complex or ambiguous for you to resolve safely. This is the ultimate safety net.
|