pdd-cli 0.0.41__py3-none-any.whl → 0.0.43__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 +1 -1
- pdd/auto_deps_main.py +1 -1
- pdd/bug_main.py +1 -1
- pdd/change_main.py +1 -1
- pdd/cli.py +81 -3
- pdd/cmd_test_main.py +3 -3
- pdd/code_generator_main.py +3 -2
- pdd/conflicts_main.py +1 -1
- pdd/construct_paths.py +245 -19
- pdd/context_generator_main.py +27 -12
- pdd/crash_main.py +44 -51
- pdd/detect_change_main.py +1 -1
- pdd/fix_code_module_errors.py +12 -0
- pdd/fix_main.py +2 -2
- pdd/fix_verification_main.py +1 -1
- pdd/generate_output_paths.py +113 -21
- pdd/generate_test.py +53 -16
- pdd/llm_invoke.py +162 -0
- pdd/preprocess_main.py +1 -1
- pdd/prompts/sync_analysis_LLM.prompt +4 -4
- pdd/split_main.py +1 -1
- pdd/sync_determine_operation.py +921 -456
- pdd/sync_main.py +333 -0
- pdd/sync_orchestration.py +639 -0
- pdd/trace_main.py +1 -1
- pdd/update_main.py +7 -2
- pdd_cli-0.0.43.dist-info/METADATA +307 -0
- {pdd_cli-0.0.41.dist-info → pdd_cli-0.0.43.dist-info}/RECORD +32 -30
- pdd_cli-0.0.41.dist-info/METADATA +0 -269
- {pdd_cli-0.0.41.dist-info → pdd_cli-0.0.43.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.41.dist-info → pdd_cli-0.0.43.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.41.dist-info → pdd_cli-0.0.43.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.41.dist-info → pdd_cli-0.0.43.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
# pdd/sync_orchestration.py
|
|
2
|
+
"""
|
|
3
|
+
Orchestrates the complete PDD sync workflow by coordinating operations and
|
|
4
|
+
animations in parallel, serving as the core engine for the `pdd sync` command.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import json
|
|
10
|
+
import datetime
|
|
11
|
+
import subprocess
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Any, Optional, List
|
|
15
|
+
from dataclasses import asdict
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
# --- Real PDD Component Imports ---
|
|
20
|
+
from .sync_animation import sync_animation
|
|
21
|
+
from .sync_determine_operation import (
|
|
22
|
+
sync_determine_operation,
|
|
23
|
+
get_pdd_file_paths,
|
|
24
|
+
RunReport,
|
|
25
|
+
PDD_DIR,
|
|
26
|
+
META_DIR,
|
|
27
|
+
SyncLock,
|
|
28
|
+
)
|
|
29
|
+
from .auto_deps_main import auto_deps_main
|
|
30
|
+
from .code_generator_main import code_generator_main
|
|
31
|
+
from .context_generator_main import context_generator_main
|
|
32
|
+
from .crash_main import crash_main
|
|
33
|
+
from .fix_verification_main import fix_verification_main
|
|
34
|
+
from .cmd_test_main import cmd_test_main
|
|
35
|
+
from .fix_main import fix_main
|
|
36
|
+
from .update_main import update_main
|
|
37
|
+
|
|
38
|
+
# --- Mock Helper Functions ---
|
|
39
|
+
|
|
40
|
+
def load_sync_log(basename: str, language: str) -> List[Dict[str, Any]]:
|
|
41
|
+
"""Load sync log entries for a basename and language."""
|
|
42
|
+
log_file = META_DIR / f"{basename}_{language}_sync.log"
|
|
43
|
+
if not log_file.exists():
|
|
44
|
+
return []
|
|
45
|
+
try:
|
|
46
|
+
with open(log_file, 'r') as f:
|
|
47
|
+
return [json.loads(line) for line in f if line.strip()]
|
|
48
|
+
except Exception:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
def save_run_report(report: Dict[str, Any], basename: str, language: str):
|
|
52
|
+
"""Save a run report to the metadata directory."""
|
|
53
|
+
report_file = META_DIR / f"{basename}_{language}_run.json"
|
|
54
|
+
META_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
with open(report_file, 'w') as f:
|
|
56
|
+
json.dump(report, f, indent=2, default=str)
|
|
57
|
+
|
|
58
|
+
def _save_operation_fingerprint(basename: str, language: str, operation: str,
|
|
59
|
+
paths: Dict[str, Path], cost: float, model: str):
|
|
60
|
+
"""Save fingerprint state after successful operation."""
|
|
61
|
+
from datetime import datetime, timezone
|
|
62
|
+
from .sync_determine_operation import calculate_current_hashes, Fingerprint
|
|
63
|
+
|
|
64
|
+
current_hashes = calculate_current_hashes(paths)
|
|
65
|
+
fingerprint = Fingerprint(
|
|
66
|
+
pdd_version="0.0.41",
|
|
67
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
68
|
+
command=operation,
|
|
69
|
+
prompt_hash=current_hashes.get('prompt_hash'),
|
|
70
|
+
code_hash=current_hashes.get('code_hash'),
|
|
71
|
+
example_hash=current_hashes.get('example_hash'),
|
|
72
|
+
test_hash=current_hashes.get('test_hash')
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
META_DIR.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
fingerprint_file = META_DIR / f"{basename}_{language}.json"
|
|
77
|
+
with open(fingerprint_file, 'w') as f:
|
|
78
|
+
json.dump(asdict(fingerprint), f, indent=2, default=str)
|
|
79
|
+
|
|
80
|
+
# SyncLock class now imported from sync_determine_operation module
|
|
81
|
+
|
|
82
|
+
def _execute_tests_and_create_run_report(test_file: Path, basename: str, language: str, target_coverage: float = 90.0) -> RunReport:
|
|
83
|
+
"""Execute tests and create a RunReport with actual results."""
|
|
84
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Execute pytest with coverage reporting on the specific module
|
|
88
|
+
# Extract module name from test file (e.g., test_factorial.py -> factorial)
|
|
89
|
+
module_name = test_file.name.replace('test_', '').replace('.py', '')
|
|
90
|
+
|
|
91
|
+
# Use the module import path rather than file path for coverage
|
|
92
|
+
result = subprocess.run([
|
|
93
|
+
'python', '-m', 'pytest',
|
|
94
|
+
str(test_file),
|
|
95
|
+
'-v',
|
|
96
|
+
'--tb=short',
|
|
97
|
+
f'--cov=pdd.{module_name}',
|
|
98
|
+
'--cov-report=term-missing'
|
|
99
|
+
], capture_output=True, text=True, timeout=300)
|
|
100
|
+
|
|
101
|
+
exit_code = result.returncode
|
|
102
|
+
stdout = result.stdout
|
|
103
|
+
stderr = result.stderr
|
|
104
|
+
|
|
105
|
+
# Parse test results from pytest output
|
|
106
|
+
tests_passed = 0
|
|
107
|
+
tests_failed = 0
|
|
108
|
+
coverage = 0.0
|
|
109
|
+
|
|
110
|
+
# Parse passed/failed tests
|
|
111
|
+
if 'passed' in stdout:
|
|
112
|
+
passed_match = re.search(r'(\d+) passed', stdout)
|
|
113
|
+
if passed_match:
|
|
114
|
+
tests_passed = int(passed_match.group(1))
|
|
115
|
+
|
|
116
|
+
if 'failed' in stdout:
|
|
117
|
+
failed_match = re.search(r'(\d+) failed', stdout)
|
|
118
|
+
if failed_match:
|
|
119
|
+
tests_failed = int(failed_match.group(1))
|
|
120
|
+
|
|
121
|
+
# Parse coverage percentage
|
|
122
|
+
coverage_match = re.search(r'TOTAL.*?(\d+)%', stdout)
|
|
123
|
+
if coverage_match:
|
|
124
|
+
coverage = float(coverage_match.group(1))
|
|
125
|
+
|
|
126
|
+
# Create and save run report
|
|
127
|
+
report = RunReport(
|
|
128
|
+
timestamp=timestamp,
|
|
129
|
+
exit_code=exit_code,
|
|
130
|
+
tests_passed=tests_passed,
|
|
131
|
+
tests_failed=tests_failed,
|
|
132
|
+
coverage=coverage
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, Exception) as e:
|
|
136
|
+
# If test execution fails, create a report indicating failure
|
|
137
|
+
report = RunReport(
|
|
138
|
+
timestamp=timestamp,
|
|
139
|
+
exit_code=1,
|
|
140
|
+
tests_passed=0,
|
|
141
|
+
tests_failed=1,
|
|
142
|
+
coverage=0.0
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Save the run report
|
|
146
|
+
save_run_report(asdict(report), basename, language)
|
|
147
|
+
return report
|
|
148
|
+
|
|
149
|
+
# --- Helper for Click Context ---
|
|
150
|
+
|
|
151
|
+
def _create_mock_context(**kwargs) -> click.Context:
|
|
152
|
+
"""Creates a mock Click context object to pass parameters to command functions."""
|
|
153
|
+
ctx = click.Context(click.Command('sync'))
|
|
154
|
+
ctx.obj = kwargs
|
|
155
|
+
return ctx
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _display_sync_log(basename: str, language: str, verbose: bool = False) -> Dict[str, Any]:
|
|
159
|
+
"""Displays the sync log for a given basename and language."""
|
|
160
|
+
log_file = META_DIR / f"{basename}_{language}_sync.log"
|
|
161
|
+
if not log_file.exists():
|
|
162
|
+
print(f"No sync log found for '{basename}' in language '{language}'.")
|
|
163
|
+
return {'success': False, 'errors': ['Log file not found.'], 'log_entries': []}
|
|
164
|
+
|
|
165
|
+
log_entries = load_sync_log(basename, language)
|
|
166
|
+
print(f"--- Sync Log for {basename} ({language}) ---")
|
|
167
|
+
|
|
168
|
+
if not log_entries:
|
|
169
|
+
print("Log is empty.")
|
|
170
|
+
return {'success': True, 'log_entries': []}
|
|
171
|
+
|
|
172
|
+
for entry in log_entries:
|
|
173
|
+
timestamp = entry.get('timestamp', 'N/A')
|
|
174
|
+
decision = entry.get('decision', {})
|
|
175
|
+
operation = decision.get('operation', 'N/A')
|
|
176
|
+
reason = decision.get('reason', 'N/A')
|
|
177
|
+
print(f"[{timestamp}] Operation: {operation:<15} | Reason: {reason}")
|
|
178
|
+
if verbose and 'details' in decision and decision['details']:
|
|
179
|
+
details_str = json.dumps(decision['details'], indent=2)
|
|
180
|
+
print(f" Details: {details_str}")
|
|
181
|
+
|
|
182
|
+
print("--- End of Log ---")
|
|
183
|
+
return {'success': True, 'log_entries': log_entries}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def sync_orchestration(
|
|
187
|
+
basename: str,
|
|
188
|
+
language: str = "python",
|
|
189
|
+
prompts_dir: str = "prompts",
|
|
190
|
+
code_dir: str = "src",
|
|
191
|
+
examples_dir: str = "examples",
|
|
192
|
+
tests_dir: str = "tests",
|
|
193
|
+
max_attempts: int = 3,
|
|
194
|
+
budget: float = 10.0,
|
|
195
|
+
skip_verify: bool = False,
|
|
196
|
+
skip_tests: bool = False,
|
|
197
|
+
target_coverage: float = 90.0,
|
|
198
|
+
log: bool = False,
|
|
199
|
+
force: bool = False,
|
|
200
|
+
strength: float = 0.5,
|
|
201
|
+
temperature: float = 0.0,
|
|
202
|
+
time_param: float = 0.25, # Renamed to avoid conflict with `time` module
|
|
203
|
+
verbose: bool = False,
|
|
204
|
+
quiet: bool = False,
|
|
205
|
+
output_cost: Optional[str] = None,
|
|
206
|
+
review_examples: bool = False,
|
|
207
|
+
local: bool = False,
|
|
208
|
+
context_config: Optional[Dict[str, str]] = None,
|
|
209
|
+
) -> Dict[str, Any]:
|
|
210
|
+
"""
|
|
211
|
+
Orchestrates the complete PDD sync workflow with parallel animation.
|
|
212
|
+
|
|
213
|
+
If log=True, displays the sync log instead of running sync operations.
|
|
214
|
+
The verbose flag controls the detail level of the log output.
|
|
215
|
+
|
|
216
|
+
Returns a dictionary summarizing the outcome of the sync process.
|
|
217
|
+
"""
|
|
218
|
+
if log:
|
|
219
|
+
return _display_sync_log(basename, language, verbose)
|
|
220
|
+
|
|
221
|
+
# --- Initialize State and Paths ---
|
|
222
|
+
try:
|
|
223
|
+
pdd_files = get_pdd_file_paths(basename, language, prompts_dir)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
# Log the error and return early with failure status
|
|
226
|
+
console.print(f"[red]Error constructing paths: {e}[/red]")
|
|
227
|
+
return {
|
|
228
|
+
"success": False,
|
|
229
|
+
"total_cost": 0.0,
|
|
230
|
+
"model_name": "",
|
|
231
|
+
"error": f"Failed to construct paths: {str(e)}",
|
|
232
|
+
"operations_completed": [],
|
|
233
|
+
"errors": [f"Path construction failed: {str(e)}"]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Shared state for animation thread
|
|
237
|
+
current_function_name_ref = ["initializing"]
|
|
238
|
+
stop_event = threading.Event()
|
|
239
|
+
current_cost_ref = [0.0]
|
|
240
|
+
prompt_path_ref = [str(pdd_files.get('prompt', 'N/A'))]
|
|
241
|
+
code_path_ref = [str(pdd_files.get('code', 'N/A'))]
|
|
242
|
+
example_path_ref = [str(pdd_files.get('example', 'N/A'))]
|
|
243
|
+
tests_path_ref = [str(pdd_files.get('test', 'N/A'))]
|
|
244
|
+
prompt_box_color_ref, code_box_color_ref, example_box_color_ref, tests_box_color_ref = \
|
|
245
|
+
["blue"], ["blue"], ["blue"], ["blue"]
|
|
246
|
+
|
|
247
|
+
# Orchestration state
|
|
248
|
+
operations_completed: List[str] = []
|
|
249
|
+
skipped_operations: List[str] = []
|
|
250
|
+
errors: List[str] = []
|
|
251
|
+
start_time = time.time()
|
|
252
|
+
animation_thread = None
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
with SyncLock(basename, language):
|
|
256
|
+
# --- Start Animation Thread ---
|
|
257
|
+
animation_thread = threading.Thread(
|
|
258
|
+
target=sync_animation,
|
|
259
|
+
args=(
|
|
260
|
+
current_function_name_ref, stop_event, basename, current_cost_ref, budget,
|
|
261
|
+
prompt_box_color_ref, code_box_color_ref, example_box_color_ref, tests_box_color_ref,
|
|
262
|
+
prompt_path_ref, code_path_ref, example_path_ref, tests_path_ref
|
|
263
|
+
),
|
|
264
|
+
daemon=True
|
|
265
|
+
)
|
|
266
|
+
animation_thread.start()
|
|
267
|
+
|
|
268
|
+
# --- Main Workflow Loop ---
|
|
269
|
+
while True:
|
|
270
|
+
if current_cost_ref[0] >= budget:
|
|
271
|
+
errors.append(f"Budget of ${budget:.2f} exceeded.")
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
decision = sync_determine_operation(basename, language, target_coverage, budget - current_cost_ref[0], False, prompts_dir, skip_tests, skip_verify)
|
|
275
|
+
operation = decision.operation
|
|
276
|
+
|
|
277
|
+
if operation in ['all_synced', 'nothing', 'fail_and_request_manual_merge', 'error', 'analyze_conflict']:
|
|
278
|
+
current_function_name_ref[0] = "synced" if operation in ['all_synced', 'nothing'] else "conflict"
|
|
279
|
+
if operation == 'fail_and_request_manual_merge':
|
|
280
|
+
errors.append(f"Manual merge required: {decision.reason}")
|
|
281
|
+
elif operation == 'error':
|
|
282
|
+
errors.append(f"Error determining operation: {decision.reason}")
|
|
283
|
+
elif operation == 'analyze_conflict':
|
|
284
|
+
errors.append(f"Conflict detected: {decision.reason}")
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
# Handle skips
|
|
288
|
+
if operation == 'verify' and (skip_verify or skip_tests):
|
|
289
|
+
# Skip verification if explicitly requested OR if tests are skipped (can't verify without tests)
|
|
290
|
+
skipped_operations.append('verify')
|
|
291
|
+
skip_reason = 'skip_verify' if skip_verify else 'skip_tests_implies_skip_verify'
|
|
292
|
+
report_data = RunReport(
|
|
293
|
+
timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
294
|
+
exit_code=0, tests_passed=0, tests_failed=0, coverage=0.0
|
|
295
|
+
)
|
|
296
|
+
save_run_report(asdict(report_data), basename, language)
|
|
297
|
+
_save_operation_fingerprint(basename, language, 'verify', pdd_files, 0.0, skip_reason)
|
|
298
|
+
continue
|
|
299
|
+
if operation == 'test' and skip_tests:
|
|
300
|
+
skipped_operations.append('test')
|
|
301
|
+
report_data = RunReport(
|
|
302
|
+
timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
303
|
+
exit_code=0, tests_passed=0, tests_failed=0, coverage=1.0
|
|
304
|
+
)
|
|
305
|
+
save_run_report(asdict(report_data), basename, language)
|
|
306
|
+
_save_operation_fingerprint(basename, language, 'test', pdd_files, 0.0, 'skipped')
|
|
307
|
+
continue
|
|
308
|
+
if operation == 'crash' and skip_tests:
|
|
309
|
+
# Skip crash operations when tests are skipped since crash fixes usually require test execution
|
|
310
|
+
skipped_operations.append('crash')
|
|
311
|
+
# Create a dummy run report indicating crash was skipped
|
|
312
|
+
report_data = RunReport(
|
|
313
|
+
timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
314
|
+
exit_code=0, tests_passed=0, tests_failed=0, coverage=0.0
|
|
315
|
+
)
|
|
316
|
+
save_run_report(asdict(report_data), basename, language)
|
|
317
|
+
_save_operation_fingerprint(basename, language, 'crash', pdd_files, 0.0, 'skipped')
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
current_function_name_ref[0] = operation
|
|
321
|
+
ctx = _create_mock_context(
|
|
322
|
+
force=force, strength=strength, temperature=temperature, time=time_param,
|
|
323
|
+
verbose=verbose, quiet=quiet, output_cost=output_cost,
|
|
324
|
+
review_examples=review_examples, local=local, budget=budget - current_cost_ref[0],
|
|
325
|
+
max_attempts=max_attempts, target_coverage=target_coverage
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
result = {}
|
|
329
|
+
success = False
|
|
330
|
+
|
|
331
|
+
# --- Execute Operation ---
|
|
332
|
+
try:
|
|
333
|
+
if operation == 'auto-deps':
|
|
334
|
+
# Save the modified prompt to a temporary location
|
|
335
|
+
temp_output = str(pdd_files['prompt']).replace('.prompt', '_with_deps.prompt')
|
|
336
|
+
|
|
337
|
+
# Read original prompt content to compare later
|
|
338
|
+
original_content = pdd_files['prompt'].read_text(encoding='utf-8')
|
|
339
|
+
|
|
340
|
+
result = auto_deps_main(
|
|
341
|
+
ctx,
|
|
342
|
+
prompt_file=str(pdd_files['prompt']),
|
|
343
|
+
directory_path=examples_dir,
|
|
344
|
+
auto_deps_csv_path="project_dependencies.csv",
|
|
345
|
+
output=temp_output,
|
|
346
|
+
force_scan=False # Don't force scan every time
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Only move the temp file back if content actually changed
|
|
350
|
+
if Path(temp_output).exists():
|
|
351
|
+
import shutil
|
|
352
|
+
new_content = Path(temp_output).read_text(encoding='utf-8')
|
|
353
|
+
if new_content != original_content:
|
|
354
|
+
shutil.move(temp_output, str(pdd_files['prompt']))
|
|
355
|
+
else:
|
|
356
|
+
# No changes needed, remove temp file
|
|
357
|
+
Path(temp_output).unlink()
|
|
358
|
+
# Mark as successful with no changes
|
|
359
|
+
result = (new_content, 0.0, 'no-changes')
|
|
360
|
+
elif operation == 'generate':
|
|
361
|
+
result = code_generator_main(
|
|
362
|
+
ctx,
|
|
363
|
+
prompt_file=str(pdd_files['prompt']),
|
|
364
|
+
output=str(pdd_files['code']),
|
|
365
|
+
original_prompt_file_path=None,
|
|
366
|
+
force_incremental_flag=False
|
|
367
|
+
)
|
|
368
|
+
elif operation == 'example':
|
|
369
|
+
print(f"DEBUG SYNC: pdd_files['example'] = {pdd_files['example']}")
|
|
370
|
+
print(f"DEBUG SYNC: str(pdd_files['example']) = {str(pdd_files['example'])}")
|
|
371
|
+
result = context_generator_main(
|
|
372
|
+
ctx,
|
|
373
|
+
prompt_file=str(pdd_files['prompt']),
|
|
374
|
+
code_file=str(pdd_files['code']),
|
|
375
|
+
output=str(pdd_files['example'])
|
|
376
|
+
)
|
|
377
|
+
elif operation == 'crash':
|
|
378
|
+
# Validate required files exist before attempting crash operation
|
|
379
|
+
required_files = [pdd_files['code'], pdd_files['example']]
|
|
380
|
+
missing_files = [f for f in required_files if not f.exists()]
|
|
381
|
+
|
|
382
|
+
if missing_files:
|
|
383
|
+
# Skip crash operation if required files are missing
|
|
384
|
+
print(f"Skipping crash operation - missing files: {[f.name for f in missing_files]}")
|
|
385
|
+
skipped_operations.append('crash')
|
|
386
|
+
# Create a dummy run report indicating crash was skipped due to missing files
|
|
387
|
+
report_data = RunReport(
|
|
388
|
+
timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
389
|
+
exit_code=0, tests_passed=0, tests_failed=0, coverage=0.0
|
|
390
|
+
)
|
|
391
|
+
save_run_report(asdict(report_data), basename, language)
|
|
392
|
+
_save_operation_fingerprint(basename, language, 'crash', pdd_files, 0.0, 'skipped_missing_files')
|
|
393
|
+
continue
|
|
394
|
+
else:
|
|
395
|
+
Path("crash.log").write_text("Simulated crash error")
|
|
396
|
+
try:
|
|
397
|
+
result = crash_main(
|
|
398
|
+
ctx,
|
|
399
|
+
prompt_file=str(pdd_files['prompt']),
|
|
400
|
+
code_file=str(pdd_files['code']),
|
|
401
|
+
program_file=str(pdd_files['example']),
|
|
402
|
+
error_file="crash.log"
|
|
403
|
+
)
|
|
404
|
+
except (RuntimeError, Exception) as e:
|
|
405
|
+
error_str = str(e)
|
|
406
|
+
if ("Simulated crash error" in error_str or
|
|
407
|
+
"LLM returned None" in error_str or
|
|
408
|
+
"LLM failed to analyze errors" in error_str):
|
|
409
|
+
# Skip crash operation for simulated errors or LLM failures
|
|
410
|
+
print(f"Skipping crash operation due to simulated/LLM error: {e}")
|
|
411
|
+
skipped_operations.append('crash')
|
|
412
|
+
report_data = RunReport(
|
|
413
|
+
timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
414
|
+
exit_code=0, tests_passed=0, tests_failed=0, coverage=0.0
|
|
415
|
+
)
|
|
416
|
+
save_run_report(asdict(report_data), basename, language)
|
|
417
|
+
_save_operation_fingerprint(basename, language, 'crash', pdd_files, 0.0, 'skipped_llm_error')
|
|
418
|
+
continue
|
|
419
|
+
else:
|
|
420
|
+
# Re-raise other exceptions
|
|
421
|
+
raise
|
|
422
|
+
elif operation == 'verify':
|
|
423
|
+
result = fix_verification_main(
|
|
424
|
+
ctx,
|
|
425
|
+
prompt_file=str(pdd_files['prompt']),
|
|
426
|
+
code_file=str(pdd_files['code']),
|
|
427
|
+
program_file=str(pdd_files['example']),
|
|
428
|
+
output_results=None,
|
|
429
|
+
output_code=str(pdd_files['code']),
|
|
430
|
+
output_program=str(pdd_files['example']),
|
|
431
|
+
loop=False,
|
|
432
|
+
verification_program=None
|
|
433
|
+
)
|
|
434
|
+
elif operation == 'test':
|
|
435
|
+
# First, generate the test file
|
|
436
|
+
result = cmd_test_main(
|
|
437
|
+
ctx,
|
|
438
|
+
prompt_file=str(pdd_files['prompt']),
|
|
439
|
+
code_file=str(pdd_files['code']),
|
|
440
|
+
output=str(pdd_files['test']),
|
|
441
|
+
language=language,
|
|
442
|
+
coverage_report=None,
|
|
443
|
+
existing_tests=None,
|
|
444
|
+
target_coverage=target_coverage,
|
|
445
|
+
merge=False
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# After successful test generation, execute the tests and create run report
|
|
449
|
+
# This enables the next sync iteration to detect test failures and trigger fix
|
|
450
|
+
if isinstance(result, dict) and result.get('success', False):
|
|
451
|
+
try:
|
|
452
|
+
test_file = pdd_files['test']
|
|
453
|
+
if test_file.exists():
|
|
454
|
+
_execute_tests_and_create_run_report(
|
|
455
|
+
test_file, basename, language, target_coverage
|
|
456
|
+
)
|
|
457
|
+
except Exception as e:
|
|
458
|
+
# Don't fail the entire operation if test execution fails
|
|
459
|
+
# Just log it - the test file generation was successful
|
|
460
|
+
print(f"Warning: Test execution failed: {e}")
|
|
461
|
+
elif isinstance(result, tuple) and len(result) >= 3:
|
|
462
|
+
# Handle tuple return format - assume success and execute tests
|
|
463
|
+
try:
|
|
464
|
+
test_file = pdd_files['test']
|
|
465
|
+
if test_file.exists():
|
|
466
|
+
_execute_tests_and_create_run_report(
|
|
467
|
+
test_file, basename, language, target_coverage
|
|
468
|
+
)
|
|
469
|
+
except Exception as e:
|
|
470
|
+
print(f"Warning: Test execution failed: {e}")
|
|
471
|
+
elif operation == 'fix':
|
|
472
|
+
# Create error file with actual test failure information
|
|
473
|
+
error_file_path = Path("fix_errors.log")
|
|
474
|
+
|
|
475
|
+
# Try to get actual test failure details from latest run
|
|
476
|
+
try:
|
|
477
|
+
from .sync_determine_operation import read_run_report
|
|
478
|
+
run_report = read_run_report(basename, language)
|
|
479
|
+
if run_report and run_report.tests_failed > 0:
|
|
480
|
+
# Run the tests again to capture actual error output
|
|
481
|
+
test_result = subprocess.run([
|
|
482
|
+
'python', '-m', 'pytest',
|
|
483
|
+
str(pdd_files['test']),
|
|
484
|
+
'-v', '--tb=short'
|
|
485
|
+
], capture_output=True, text=True, timeout=300)
|
|
486
|
+
|
|
487
|
+
error_content = f"Test failures detected ({run_report.tests_failed} failed tests):\n\n"
|
|
488
|
+
error_content += "STDOUT:\n" + test_result.stdout + "\n\n"
|
|
489
|
+
error_content += "STDERR:\n" + test_result.stderr
|
|
490
|
+
else:
|
|
491
|
+
error_content = "Simulated test failures"
|
|
492
|
+
except Exception as e:
|
|
493
|
+
error_content = f"Could not capture test failures: {e}\nUsing simulated test failures"
|
|
494
|
+
|
|
495
|
+
error_file_path.write_text(error_content)
|
|
496
|
+
|
|
497
|
+
result = fix_main(
|
|
498
|
+
ctx,
|
|
499
|
+
prompt_file=str(pdd_files['prompt']),
|
|
500
|
+
code_file=str(pdd_files['code']),
|
|
501
|
+
unit_test_file=str(pdd_files['test']),
|
|
502
|
+
error_file=str(error_file_path),
|
|
503
|
+
output_test=str(pdd_files['test']),
|
|
504
|
+
output_code=str(pdd_files['code']),
|
|
505
|
+
output_results=f"{basename}_fix_results.log",
|
|
506
|
+
loop=False,
|
|
507
|
+
verification_program=None,
|
|
508
|
+
max_attempts=max_attempts,
|
|
509
|
+
budget=budget - current_cost_ref[0],
|
|
510
|
+
auto_submit=False
|
|
511
|
+
)
|
|
512
|
+
elif operation == 'update':
|
|
513
|
+
result = update_main(
|
|
514
|
+
ctx,
|
|
515
|
+
input_prompt_file=str(pdd_files['prompt']),
|
|
516
|
+
modified_code_file=str(pdd_files['code']),
|
|
517
|
+
input_code_file=None,
|
|
518
|
+
output=str(pdd_files['prompt']),
|
|
519
|
+
git=True
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
errors.append(f"Unknown operation '{operation}' requested.")
|
|
523
|
+
result = {'success': False, 'cost': 0.0}
|
|
524
|
+
|
|
525
|
+
# Handle different return formats from command functions
|
|
526
|
+
if isinstance(result, dict):
|
|
527
|
+
# Dictionary return (e.g., from some commands)
|
|
528
|
+
success = result.get('success', False)
|
|
529
|
+
current_cost_ref[0] += result.get('cost', 0.0)
|
|
530
|
+
elif isinstance(result, tuple) and len(result) >= 3:
|
|
531
|
+
# Tuple return (e.g., from code_generator_main, context_generator_main)
|
|
532
|
+
# For tuples, success is determined by no exceptions and valid return content
|
|
533
|
+
# Check if the first element (generated content) is None, which indicates failure
|
|
534
|
+
success = result[0] is not None
|
|
535
|
+
# Extract cost from tuple (usually second-to-last element)
|
|
536
|
+
cost = result[-2] if len(result) >= 2 and isinstance(result[-2], (int, float)) else 0.0
|
|
537
|
+
current_cost_ref[0] += cost
|
|
538
|
+
else:
|
|
539
|
+
# Unknown return format
|
|
540
|
+
success = result is not None
|
|
541
|
+
current_cost_ref[0] += 0.0
|
|
542
|
+
|
|
543
|
+
except Exception as e:
|
|
544
|
+
errors.append(f"Exception during '{operation}': {e}")
|
|
545
|
+
success = False
|
|
546
|
+
|
|
547
|
+
if success:
|
|
548
|
+
operations_completed.append(operation)
|
|
549
|
+
# Extract cost and model from result based on format
|
|
550
|
+
if isinstance(result, dict):
|
|
551
|
+
cost = result.get('cost', 0.0)
|
|
552
|
+
model = result.get('model', '')
|
|
553
|
+
elif isinstance(result, tuple) and len(result) >= 3:
|
|
554
|
+
cost = result[-2] if len(result) >= 2 and isinstance(result[-2], (int, float)) else 0.0
|
|
555
|
+
model = result[-1] if len(result) >= 1 and isinstance(result[-1], str) else ''
|
|
556
|
+
else:
|
|
557
|
+
cost = 0.0
|
|
558
|
+
model = ''
|
|
559
|
+
_save_operation_fingerprint(basename, language, operation, pdd_files, cost, model)
|
|
560
|
+
|
|
561
|
+
# After successful fix operation, execute tests to update run report
|
|
562
|
+
if operation == 'fix':
|
|
563
|
+
try:
|
|
564
|
+
test_file = pdd_files['test']
|
|
565
|
+
if test_file.exists():
|
|
566
|
+
_execute_tests_and_create_run_report(
|
|
567
|
+
test_file, basename, language, target_coverage
|
|
568
|
+
)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
# Don't fail the entire operation if test execution fails
|
|
571
|
+
print(f"Warning: Post-fix test execution failed: {e}")
|
|
572
|
+
else:
|
|
573
|
+
errors.append(f"Operation '{operation}' failed.")
|
|
574
|
+
break
|
|
575
|
+
|
|
576
|
+
except TimeoutError:
|
|
577
|
+
errors.append(f"Could not acquire lock for '{basename}'. Another sync process may be running.")
|
|
578
|
+
except Exception as e:
|
|
579
|
+
errors.append(f"An unexpected error occurred in the orchestrator: {e}")
|
|
580
|
+
finally:
|
|
581
|
+
if stop_event:
|
|
582
|
+
stop_event.set()
|
|
583
|
+
if animation_thread and animation_thread.is_alive():
|
|
584
|
+
animation_thread.join(timeout=5)
|
|
585
|
+
|
|
586
|
+
total_time = time.time() - start_time
|
|
587
|
+
final_state = {
|
|
588
|
+
p_name: {'exists': p_path.exists(), 'path': str(p_path)}
|
|
589
|
+
for p_name, p_path in pdd_files.items()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
'success': not errors,
|
|
594
|
+
'operations_completed': operations_completed,
|
|
595
|
+
'skipped_operations': skipped_operations,
|
|
596
|
+
'total_cost': current_cost_ref[0],
|
|
597
|
+
'total_time': total_time,
|
|
598
|
+
'final_state': final_state,
|
|
599
|
+
'errors': errors,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if __name__ == '__main__':
|
|
603
|
+
# Example usage of the sync_orchestration module.
|
|
604
|
+
# This simulates running `pdd sync my_calculator` from the command line.
|
|
605
|
+
|
|
606
|
+
print("--- Running Basic Sync Orchestration Example ---")
|
|
607
|
+
|
|
608
|
+
# Setup a dummy project structure
|
|
609
|
+
Path("./prompts").mkdir(exist_ok=True)
|
|
610
|
+
Path("./src").mkdir(exist_ok=True)
|
|
611
|
+
Path("./examples").mkdir(exist_ok=True)
|
|
612
|
+
Path("./tests").mkdir(exist_ok=True)
|
|
613
|
+
Path("./prompts/my_calculator_python.prompt").write_text("Create a calculator.")
|
|
614
|
+
|
|
615
|
+
# Ensure PDD meta directory exists for logs and locks
|
|
616
|
+
PDD_DIR.mkdir(exist_ok=True)
|
|
617
|
+
META_DIR.mkdir(exist_ok=True)
|
|
618
|
+
|
|
619
|
+
result = sync_orchestration(
|
|
620
|
+
basename="my_calculator",
|
|
621
|
+
language="python",
|
|
622
|
+
quiet=True # Suppress mock command output for cleaner example run
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
print("\n--- Sync Orchestration Finished ---")
|
|
626
|
+
print(json.dumps(result, indent=2))
|
|
627
|
+
|
|
628
|
+
if result['success']:
|
|
629
|
+
print("\n✅ Sync completed successfully.")
|
|
630
|
+
else:
|
|
631
|
+
print(f"\n❌ Sync failed. Errors: {result['errors']}")
|
|
632
|
+
|
|
633
|
+
print("\n--- Running Sync Log Example ---")
|
|
634
|
+
# This will now show the log from the run we just completed.
|
|
635
|
+
log_result = sync_orchestration(
|
|
636
|
+
basename="my_calculator",
|
|
637
|
+
language="python",
|
|
638
|
+
log=True
|
|
639
|
+
)
|
pdd/trace_main.py
CHANGED
|
@@ -34,7 +34,7 @@ def trace_main(ctx: click.Context, prompt_file: str, code_file: str, code_line:
|
|
|
34
34
|
command_options = {
|
|
35
35
|
"output": output
|
|
36
36
|
}
|
|
37
|
-
input_strings, output_file_paths, _ = construct_paths(
|
|
37
|
+
resolved_config, input_strings, output_file_paths, _ = construct_paths(
|
|
38
38
|
input_file_paths=input_file_paths,
|
|
39
39
|
force=ctx.obj.get('force', False),
|
|
40
40
|
quiet=quiet,
|
pdd/update_main.py
CHANGED
|
@@ -36,8 +36,13 @@ def update_main(
|
|
|
36
36
|
if not git and input_code_file is None:
|
|
37
37
|
raise ValueError("Must provide an input code file or use --git option.")
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if output is None:
|
|
40
|
+
# Default to overwriting the original prompt file when no explicit output specified
|
|
41
|
+
# This preserves the "prompts as source of truth" philosophy
|
|
42
|
+
command_options = {"output": input_prompt_file}
|
|
43
|
+
else:
|
|
44
|
+
command_options = {"output": output}
|
|
45
|
+
resolved_config, input_strings, output_file_paths, _ = construct_paths(
|
|
41
46
|
input_file_paths=input_file_paths,
|
|
42
47
|
force=ctx.obj.get("force", False),
|
|
43
48
|
quiet=ctx.obj.get("quiet", False),
|