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
pdd/sync_main.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Tuple
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich import print as rprint
|
|
11
|
+
|
|
12
|
+
# Relative imports from the pdd package
|
|
13
|
+
from . import DEFAULT_STRENGTH, DEFAULT_TIME
|
|
14
|
+
from .construct_paths import (
|
|
15
|
+
_is_known_language,
|
|
16
|
+
construct_paths,
|
|
17
|
+
_find_pddrc_file,
|
|
18
|
+
_load_pddrc_config,
|
|
19
|
+
_detect_context,
|
|
20
|
+
_get_context_config
|
|
21
|
+
)
|
|
22
|
+
from .sync_orchestration import sync_orchestration
|
|
23
|
+
|
|
24
|
+
# A simple regex for basename validation to prevent path traversal or other injection
|
|
25
|
+
VALID_BASENAME_CHARS = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_basename(basename: str) -> None:
|
|
29
|
+
"""Raises UsageError if the basename is invalid."""
|
|
30
|
+
if not basename:
|
|
31
|
+
raise click.UsageError("BASENAME cannot be empty.")
|
|
32
|
+
if not VALID_BASENAME_CHARS.match(basename):
|
|
33
|
+
raise click.UsageError(
|
|
34
|
+
f"Basename '{basename}' contains invalid characters. "
|
|
35
|
+
"Only alphanumeric, underscore, and hyphen are allowed."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _detect_languages(basename: str, prompts_dir: Path) -> List[str]:
|
|
40
|
+
"""
|
|
41
|
+
Detects all available languages for a given basename by finding
|
|
42
|
+
matching prompt files in the prompts directory.
|
|
43
|
+
Excludes runtime languages (LLM) as they cannot form valid development units.
|
|
44
|
+
"""
|
|
45
|
+
development_languages = []
|
|
46
|
+
if not prompts_dir.is_dir():
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
pattern = f"{basename}_*.prompt"
|
|
50
|
+
for prompt_file in prompts_dir.glob(pattern):
|
|
51
|
+
# stem is 'basename_language'
|
|
52
|
+
stem = prompt_file.stem
|
|
53
|
+
# Ensure the file starts with the exact basename followed by an underscore
|
|
54
|
+
if stem.startswith(f"{basename}_"):
|
|
55
|
+
potential_language = stem[len(basename) + 1 :]
|
|
56
|
+
try:
|
|
57
|
+
if _is_known_language(potential_language):
|
|
58
|
+
# Exclude runtime languages (LLM) as they cannot form valid development units
|
|
59
|
+
if potential_language.lower() != 'llm':
|
|
60
|
+
development_languages.append(potential_language)
|
|
61
|
+
except ValueError:
|
|
62
|
+
# PDD_PATH not set (likely during testing) - assume language is valid
|
|
63
|
+
# if it matches common language patterns
|
|
64
|
+
common_languages = {"python", "javascript", "java", "cpp", "c", "go", "rust", "typescript"}
|
|
65
|
+
if potential_language.lower() in common_languages:
|
|
66
|
+
development_languages.append(potential_language)
|
|
67
|
+
# Explicitly exclude 'llm' even in test scenarios
|
|
68
|
+
|
|
69
|
+
# Return only development languages, with Python prioritized first, then sorted alphabetically
|
|
70
|
+
if 'python' in development_languages:
|
|
71
|
+
# Put Python first, then the rest sorted alphabetically
|
|
72
|
+
other_languages = sorted([lang for lang in development_languages if lang != 'python'])
|
|
73
|
+
return ['python'] + other_languages
|
|
74
|
+
else:
|
|
75
|
+
# No Python, just return sorted alphabetically
|
|
76
|
+
return sorted(development_languages)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def sync_main(
|
|
80
|
+
ctx: click.Context,
|
|
81
|
+
basename: str,
|
|
82
|
+
max_attempts: int,
|
|
83
|
+
budget: float,
|
|
84
|
+
skip_verify: bool,
|
|
85
|
+
skip_tests: bool,
|
|
86
|
+
target_coverage: float,
|
|
87
|
+
log: bool,
|
|
88
|
+
) -> Tuple[Dict[str, Any], float, str]:
|
|
89
|
+
"""
|
|
90
|
+
CLI wrapper for the sync command. Handles parameter validation, path construction,
|
|
91
|
+
language detection, and orchestrates the sync workflow for each detected language.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
ctx: The Click context object.
|
|
95
|
+
basename: The base name for the prompt file.
|
|
96
|
+
max_attempts: Maximum number of fix attempts.
|
|
97
|
+
budget: Maximum total cost for the sync process.
|
|
98
|
+
skip_verify: Skip the functional verification step.
|
|
99
|
+
skip_tests: Skip unit test generation and fixing.
|
|
100
|
+
target_coverage: Desired code coverage percentage.
|
|
101
|
+
log: If True, display sync logs instead of running the sync.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A tuple containing the results dictionary, total cost, and primary model name.
|
|
105
|
+
"""
|
|
106
|
+
console = Console()
|
|
107
|
+
start_time = time.time()
|
|
108
|
+
|
|
109
|
+
# 1. Retrieve global parameters from context
|
|
110
|
+
strength = ctx.obj.get("strength", DEFAULT_STRENGTH)
|
|
111
|
+
temperature = ctx.obj.get("temperature", 0.0)
|
|
112
|
+
time_param = ctx.obj.get("time", DEFAULT_TIME)
|
|
113
|
+
verbose = ctx.obj.get("verbose", False)
|
|
114
|
+
force = ctx.obj.get("force", False)
|
|
115
|
+
quiet = ctx.obj.get("quiet", False)
|
|
116
|
+
output_cost = ctx.obj.get("output_cost", None)
|
|
117
|
+
review_examples = ctx.obj.get("review_examples", False)
|
|
118
|
+
local = ctx.obj.get("local", False)
|
|
119
|
+
context_override = ctx.obj.get("context", None)
|
|
120
|
+
|
|
121
|
+
# 2. Validate inputs
|
|
122
|
+
_validate_basename(basename)
|
|
123
|
+
if budget <= 0:
|
|
124
|
+
raise click.BadParameter("Budget must be a positive number.", param_hint="--budget")
|
|
125
|
+
if max_attempts <= 0:
|
|
126
|
+
raise click.BadParameter("Max attempts must be a positive integer.", param_hint="--max-attempts")
|
|
127
|
+
|
|
128
|
+
if not quiet and budget < 1.0:
|
|
129
|
+
console.log(f"[yellow]Warning:[/] Budget of ${budget:.2f} is low. Complex operations may exceed this limit.")
|
|
130
|
+
|
|
131
|
+
# 3. Use construct_paths in 'discovery' mode to find the prompts directory.
|
|
132
|
+
try:
|
|
133
|
+
initial_config, _, _, _ = construct_paths(
|
|
134
|
+
input_file_paths={},
|
|
135
|
+
force=False,
|
|
136
|
+
quiet=True,
|
|
137
|
+
command="sync",
|
|
138
|
+
command_options={"basename": basename},
|
|
139
|
+
context_override=context_override,
|
|
140
|
+
)
|
|
141
|
+
prompts_dir = Path(initial_config.get("prompts_dir", "prompts"))
|
|
142
|
+
except Exception as e:
|
|
143
|
+
rprint(f"[bold red]Error initializing PDD paths:[/bold red] {e}")
|
|
144
|
+
raise click.Abort()
|
|
145
|
+
|
|
146
|
+
# 4. Detect all languages for the given basename
|
|
147
|
+
languages = _detect_languages(basename, prompts_dir)
|
|
148
|
+
if not languages:
|
|
149
|
+
raise click.UsageError(
|
|
150
|
+
f"No prompt files found for basename '{basename}' in directory '{prompts_dir}'.\n"
|
|
151
|
+
f"Expected files with format: '{basename}_<language>.prompt'"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# 5. Handle --log mode separately
|
|
155
|
+
if log:
|
|
156
|
+
if not quiet:
|
|
157
|
+
rprint(Panel(f"Displaying sync logs for [bold cyan]{basename}[/bold cyan]", title="PDD Sync Log", expand=False))
|
|
158
|
+
|
|
159
|
+
for lang in languages:
|
|
160
|
+
if not quiet:
|
|
161
|
+
rprint(f"\n--- Log for language: [bold green]{lang}[/bold green] ---")
|
|
162
|
+
|
|
163
|
+
# Use construct_paths to get proper directory configuration for log mode
|
|
164
|
+
prompt_file_path = prompts_dir / f"{basename}_{lang}.prompt"
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
resolved_config, _, _, _ = construct_paths(
|
|
168
|
+
input_file_paths={"prompt_file": str(prompt_file_path)},
|
|
169
|
+
force=force,
|
|
170
|
+
quiet=True,
|
|
171
|
+
command="sync",
|
|
172
|
+
command_options={"basename": basename, "language": lang},
|
|
173
|
+
context_override=context_override,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
code_dir = resolved_config.get("code_dir", "src")
|
|
177
|
+
tests_dir = resolved_config.get("tests_dir", "tests")
|
|
178
|
+
examples_dir = resolved_config.get("examples_dir", "examples")
|
|
179
|
+
except Exception:
|
|
180
|
+
# Fallback to default paths if construct_paths fails
|
|
181
|
+
code_dir = str(prompts_dir.parent / "src")
|
|
182
|
+
tests_dir = str(prompts_dir.parent / "tests")
|
|
183
|
+
examples_dir = str(prompts_dir.parent / "examples")
|
|
184
|
+
|
|
185
|
+
sync_orchestration(
|
|
186
|
+
basename=basename,
|
|
187
|
+
language=lang,
|
|
188
|
+
prompts_dir=str(prompts_dir),
|
|
189
|
+
code_dir=str(code_dir),
|
|
190
|
+
examples_dir=str(examples_dir),
|
|
191
|
+
tests_dir=str(tests_dir),
|
|
192
|
+
log=True,
|
|
193
|
+
verbose=verbose,
|
|
194
|
+
quiet=quiet,
|
|
195
|
+
)
|
|
196
|
+
return {}, 0.0, ""
|
|
197
|
+
|
|
198
|
+
# 6. Main Sync Workflow
|
|
199
|
+
if not quiet:
|
|
200
|
+
summary_panel = Panel(
|
|
201
|
+
f"Basename: [bold cyan]{basename}[/bold cyan]\n"
|
|
202
|
+
f"Languages: [bold green]{', '.join(languages)}[/bold green]\n"
|
|
203
|
+
f"Budget: [bold yellow]${budget:.2f}[/bold yellow]\n"
|
|
204
|
+
f"Max Attempts: [bold blue]{max_attempts}[/bold blue]",
|
|
205
|
+
title="PDD Sync Starting",
|
|
206
|
+
expand=False,
|
|
207
|
+
)
|
|
208
|
+
rprint(summary_panel)
|
|
209
|
+
|
|
210
|
+
aggregated_results: Dict[str, Any] = {"results_by_language": {}}
|
|
211
|
+
total_cost = 0.0
|
|
212
|
+
primary_model = ""
|
|
213
|
+
overall_success = True
|
|
214
|
+
remaining_budget = budget
|
|
215
|
+
|
|
216
|
+
for lang in languages:
|
|
217
|
+
if not quiet:
|
|
218
|
+
rprint(f"\n[bold]🚀 Syncing for language: [green]{lang}[/green]...[/bold]")
|
|
219
|
+
|
|
220
|
+
if remaining_budget <= 0:
|
|
221
|
+
if not quiet:
|
|
222
|
+
rprint(f"[yellow]Budget exhausted. Skipping sync for '{lang}'.[/yellow]")
|
|
223
|
+
overall_success = False
|
|
224
|
+
aggregated_results["results_by_language"][lang] = {"success": False, "error": "Budget exhausted"}
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Get the fully resolved configuration for this specific language using construct_paths.
|
|
229
|
+
prompt_file_path = prompts_dir / f"{basename}_{lang}.prompt"
|
|
230
|
+
|
|
231
|
+
command_options = {
|
|
232
|
+
"basename": basename,
|
|
233
|
+
"language": lang,
|
|
234
|
+
"max_attempts": max_attempts,
|
|
235
|
+
"budget": budget,
|
|
236
|
+
"target_coverage": target_coverage,
|
|
237
|
+
"strength": strength,
|
|
238
|
+
"temperature": temperature,
|
|
239
|
+
"time": time_param,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
resolved_config, _, _, resolved_language = construct_paths(
|
|
243
|
+
input_file_paths={"prompt_file": str(prompt_file_path)},
|
|
244
|
+
force=force,
|
|
245
|
+
quiet=True,
|
|
246
|
+
command="sync",
|
|
247
|
+
command_options=command_options,
|
|
248
|
+
context_override=context_override,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Extract all parameters directly from the resolved configuration
|
|
252
|
+
final_strength = resolved_config.get("strength", strength)
|
|
253
|
+
final_temp = resolved_config.get("temperature", temperature)
|
|
254
|
+
final_max_attempts = resolved_config.get("max_attempts", max_attempts)
|
|
255
|
+
final_target_coverage = resolved_config.get("target_coverage", target_coverage)
|
|
256
|
+
|
|
257
|
+
code_dir = resolved_config.get("code_dir", "src")
|
|
258
|
+
tests_dir = resolved_config.get("tests_dir", "tests")
|
|
259
|
+
examples_dir = resolved_config.get("examples_dir", "examples")
|
|
260
|
+
|
|
261
|
+
sync_result = sync_orchestration(
|
|
262
|
+
basename=basename,
|
|
263
|
+
language=resolved_language,
|
|
264
|
+
prompts_dir=str(prompts_dir),
|
|
265
|
+
code_dir=str(code_dir),
|
|
266
|
+
examples_dir=str(examples_dir),
|
|
267
|
+
tests_dir=str(tests_dir),
|
|
268
|
+
budget=remaining_budget,
|
|
269
|
+
max_attempts=final_max_attempts,
|
|
270
|
+
skip_verify=skip_verify,
|
|
271
|
+
skip_tests=skip_tests,
|
|
272
|
+
target_coverage=final_target_coverage,
|
|
273
|
+
strength=final_strength,
|
|
274
|
+
temperature=final_temp,
|
|
275
|
+
time_param=time_param,
|
|
276
|
+
force=force,
|
|
277
|
+
quiet=quiet,
|
|
278
|
+
verbose=verbose,
|
|
279
|
+
output_cost=output_cost,
|
|
280
|
+
review_examples=review_examples,
|
|
281
|
+
local=local,
|
|
282
|
+
context_config=resolved_config,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
lang_cost = sync_result.get("total_cost", 0.0)
|
|
286
|
+
total_cost += lang_cost
|
|
287
|
+
remaining_budget -= lang_cost
|
|
288
|
+
|
|
289
|
+
if sync_result.get("model_name"):
|
|
290
|
+
primary_model = sync_result["model_name"]
|
|
291
|
+
|
|
292
|
+
if not sync_result.get("success", False):
|
|
293
|
+
overall_success = False
|
|
294
|
+
|
|
295
|
+
aggregated_results["results_by_language"][lang] = sync_result
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
if not quiet:
|
|
299
|
+
rprint(f"[bold red]An unexpected error occurred during sync for '{lang}':[/bold red] {e}")
|
|
300
|
+
if verbose:
|
|
301
|
+
console.print_exception(show_locals=True)
|
|
302
|
+
overall_success = False
|
|
303
|
+
aggregated_results["results_by_language"][lang] = {"success": False, "error": str(e)}
|
|
304
|
+
|
|
305
|
+
# 7. Final Summary Report
|
|
306
|
+
if not quiet:
|
|
307
|
+
elapsed_time = time.time() - start_time
|
|
308
|
+
final_table = Table(title="PDD Sync Complete", show_header=True, header_style="bold magenta")
|
|
309
|
+
final_table.add_column("Language", style="cyan", no_wrap=True)
|
|
310
|
+
final_table.add_column("Status", justify="center")
|
|
311
|
+
final_table.add_column("Cost (USD)", justify="right", style="yellow")
|
|
312
|
+
final_table.add_column("Details")
|
|
313
|
+
|
|
314
|
+
for lang, result in aggregated_results["results_by_language"].items():
|
|
315
|
+
status = "[green]Success[/green]" if result.get("success") else "[red]Failed[/red]"
|
|
316
|
+
cost_str = f"${result.get('total_cost', 0.0):.4f}"
|
|
317
|
+
details = result.get("summary") or result.get("error", "No details.")
|
|
318
|
+
final_table.add_row(lang, status, cost_str, str(details))
|
|
319
|
+
|
|
320
|
+
rprint(final_table)
|
|
321
|
+
|
|
322
|
+
summary_text = (
|
|
323
|
+
f"Total time: [bold]{elapsed_time:.2f}s[/bold] | "
|
|
324
|
+
f"Total cost: [bold yellow]${total_cost:.4f}[/bold yellow] | "
|
|
325
|
+
f"Overall status: {'[green]Success[/green]' if overall_success else '[red]Failed[/red]'}"
|
|
326
|
+
)
|
|
327
|
+
rprint(Panel(summary_text, expand=False))
|
|
328
|
+
|
|
329
|
+
aggregated_results["overall_success"] = overall_success
|
|
330
|
+
aggregated_results["total_cost"] = total_cost
|
|
331
|
+
aggregated_results["primary_model"] = primary_model
|
|
332
|
+
|
|
333
|
+
return aggregated_results, total_cost, primary_model
|