pdd-cli 0.0.40__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/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
- quiet=True # Suppress individual change prints for CSV mode
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: ```{program_code_fix}```
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.