pdd-cli 0.0.40__py3-none-any.whl → 0.0.42__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. pdd/__init__.py +1 -1
  2. pdd/auto_deps_main.py +1 -1
  3. pdd/auto_update.py +73 -78
  4. pdd/bug_main.py +3 -3
  5. pdd/bug_to_unit_test.py +46 -38
  6. pdd/change.py +20 -13
  7. pdd/change_main.py +223 -163
  8. pdd/cli.py +192 -95
  9. pdd/cmd_test_main.py +51 -36
  10. pdd/code_generator_main.py +3 -2
  11. pdd/conflicts_main.py +1 -1
  12. pdd/construct_paths.py +221 -19
  13. pdd/context_generator_main.py +27 -12
  14. pdd/crash_main.py +44 -50
  15. pdd/data/llm_model.csv +1 -1
  16. pdd/detect_change_main.py +1 -1
  17. pdd/fix_code_module_errors.py +12 -0
  18. pdd/fix_main.py +2 -2
  19. pdd/fix_verification_errors.py +13 -0
  20. pdd/fix_verification_main.py +3 -3
  21. pdd/generate_output_paths.py +113 -21
  22. pdd/generate_test.py +53 -16
  23. pdd/llm_invoke.py +162 -0
  24. pdd/logo_animation.py +455 -0
  25. pdd/preprocess_main.py +1 -1
  26. pdd/process_csv_change.py +1 -1
  27. pdd/prompts/extract_program_code_fix_LLM.prompt +2 -1
  28. pdd/prompts/sync_analysis_LLM.prompt +82 -0
  29. pdd/split_main.py +1 -1
  30. pdd/sync_animation.py +643 -0
  31. pdd/sync_determine_operation.py +1039 -0
  32. pdd/sync_main.py +333 -0
  33. pdd/sync_orchestration.py +639 -0
  34. pdd/trace_main.py +1 -1
  35. pdd/update_main.py +7 -2
  36. pdd/xml_tagger.py +15 -6
  37. pdd_cli-0.0.42.dist-info/METADATA +307 -0
  38. {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/RECORD +42 -36
  39. pdd_cli-0.0.40.dist-info/METADATA +0 -269
  40. {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/WHEEL +0 -0
  41. {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/entry_points.txt +0 -0
  42. {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/licenses/LICENSE +0 -0
  43. {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/top_level.txt +0 -0
pdd/construct_paths.py CHANGED
@@ -4,9 +4,12 @@ from __future__ import annotations
4
4
  import sys
5
5
  import os
6
6
  from pathlib import Path
7
- from typing import Dict, Tuple, Any, Optional # Added Optional
7
+ from typing import Dict, Tuple, Any, Optional, List
8
+ import fnmatch
9
+ import logging
8
10
 
9
11
  import click
12
+ import yaml
10
13
  from rich.console import Console
11
14
  from rich.theme import Theme
12
15
 
@@ -21,6 +24,114 @@ import csv
21
24
 
22
25
  console = Console(theme=Theme({"info": "cyan", "warning": "yellow", "error": "bold red"}))
23
26
 
27
+ # Configuration loading functions
28
+ def _find_pddrc_file(start_path: Optional[Path] = None) -> Optional[Path]:
29
+ """Find .pddrc file by searching upward from the given path."""
30
+ if start_path is None:
31
+ start_path = Path.cwd()
32
+
33
+ # Search upward through parent directories
34
+ for path in [start_path] + list(start_path.parents):
35
+ pddrc_file = path / ".pddrc"
36
+ if pddrc_file.is_file():
37
+ return pddrc_file
38
+ return None
39
+
40
+ def _load_pddrc_config(pddrc_path: Path) -> Dict[str, Any]:
41
+ """Load and parse .pddrc configuration file."""
42
+ try:
43
+ with open(pddrc_path, 'r', encoding='utf-8') as f:
44
+ config = yaml.safe_load(f)
45
+
46
+ if not isinstance(config, dict):
47
+ raise ValueError(f"Invalid .pddrc format: expected dictionary at root level")
48
+
49
+ # Validate basic structure
50
+ if 'contexts' not in config:
51
+ raise ValueError(f"Invalid .pddrc format: missing 'contexts' section")
52
+
53
+ return config
54
+ except yaml.YAMLError as e:
55
+ raise ValueError(f"YAML syntax error in .pddrc: {e}")
56
+ except Exception as e:
57
+ raise ValueError(f"Error loading .pddrc: {e}")
58
+
59
+ def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
60
+ """Detect the appropriate context based on current directory path."""
61
+ if context_override:
62
+ # Validate that the override context exists
63
+ contexts = config.get('contexts', {})
64
+ if context_override not in contexts:
65
+ available = list(contexts.keys())
66
+ raise ValueError(f"Unknown context '{context_override}'. Available contexts: {available}")
67
+ return context_override
68
+
69
+ contexts = config.get('contexts', {})
70
+ current_path_str = str(current_dir)
71
+
72
+ # Try to match against each context's paths
73
+ for context_name, context_config in contexts.items():
74
+ if context_name == 'default':
75
+ continue # Handle default as fallback
76
+
77
+ paths = context_config.get('paths', [])
78
+ for path_pattern in paths:
79
+ # Convert glob pattern to match current directory
80
+ if fnmatch.fnmatch(current_path_str, f"*/{path_pattern}") or \
81
+ fnmatch.fnmatch(current_path_str, path_pattern) or \
82
+ current_path_str.endswith(f"/{path_pattern.rstrip('/**')}"):
83
+ return context_name
84
+
85
+ # Return default context if available
86
+ if 'default' in contexts:
87
+ return 'default'
88
+
89
+ return None
90
+
91
+ def _get_context_config(config: Dict[str, Any], context_name: Optional[str]) -> Dict[str, Any]:
92
+ """Get configuration settings for the specified context."""
93
+ if not context_name:
94
+ return {}
95
+
96
+ contexts = config.get('contexts', {})
97
+ context_config = contexts.get(context_name, {})
98
+ return context_config.get('defaults', {})
99
+
100
+ def _resolve_config_hierarchy(
101
+ cli_options: Dict[str, Any],
102
+ context_config: Dict[str, Any],
103
+ env_vars: Dict[str, str]
104
+ ) -> Dict[str, Any]:
105
+ """Apply configuration hierarchy: CLI > context > environment > defaults."""
106
+ resolved = {}
107
+
108
+ # Configuration keys to resolve
109
+ config_keys = {
110
+ 'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
111
+ 'test_output_path': 'PDD_TEST_OUTPUT_PATH',
112
+ 'example_output_path': 'PDD_EXAMPLE_OUTPUT_PATH',
113
+ 'default_language': 'PDD_DEFAULT_LANGUAGE',
114
+ 'target_coverage': 'PDD_TEST_COVERAGE_TARGET',
115
+ 'strength': None,
116
+ 'temperature': None,
117
+ 'budget': None,
118
+ 'max_attempts': None,
119
+ }
120
+
121
+ for config_key, env_var in config_keys.items():
122
+ # 1. CLI options (highest priority)
123
+ if config_key in cli_options and cli_options[config_key] is not None:
124
+ resolved[config_key] = cli_options[config_key]
125
+ # 2. Context configuration
126
+ elif config_key in context_config:
127
+ resolved[config_key] = context_config[config_key]
128
+ # 3. Environment variables
129
+ elif env_var and env_var in env_vars:
130
+ resolved[config_key] = env_vars[env_var]
131
+ # 4. Defaults are handled elsewhere
132
+
133
+ return resolved
134
+
24
135
 
25
136
  def _read_file(path: Path) -> str:
26
137
  """Read a text file safely and return its contents."""
@@ -126,29 +237,24 @@ def _is_known_language(language_name: str) -> bool:
126
237
 
127
238
  def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
128
239
  """
129
- Remove trailing '_<language>.prompt' or '_<language>' from a filename stem
130
- if it matches a known language.
240
+ Remove trailing '_<language>' from a filename stem if it matches a known language.
131
241
  """
132
242
  p = Path(path_like)
133
- stem = p.stem # removes last extension (e.g. '.prompt', '.py')
243
+ stem = p.stem # removes last extension (e.g., '.prompt', '.py')
134
244
 
135
- if "_" not in stem: # No underscore, nothing to strip
245
+ if "_" not in stem:
136
246
  return stem
137
247
 
138
248
  parts = stem.split("_")
139
- # Avoid splitting single-word stems like "Makefile_" if that's possible
140
- if len(parts) < 2:
141
- return stem
142
-
143
249
  candidate_lang = parts[-1]
144
250
 
145
- # Check if the last part is a known language
146
251
  if _is_known_language(candidate_lang):
147
- # If the last part is a language, strip it
252
+ # Do not strip '_prompt' from a non-.prompt file (e.g., 'test_prompt.txt')
253
+ if candidate_lang == 'prompt' and p.suffix != '.prompt':
254
+ return stem
148
255
  return "_".join(parts[:-1])
149
- else:
150
- # Last part is not a language, return original stem
151
- return stem
256
+
257
+ return stem
152
258
 
153
259
 
154
260
  def _extract_basename(
@@ -267,20 +373,101 @@ def construct_paths(
267
373
  command: str,
268
374
  command_options: Optional[Dict[str, Any]], # Allow None
269
375
  create_error_file: bool = True, # Added parameter to control error file creation
270
- ) -> Tuple[Dict[str, str], Dict[str, str], str]:
376
+ context_override: Optional[str] = None, # Added parameter for context override
377
+ ) -> Tuple[Dict[str, Any], Dict[str, str], Dict[str, str], str]:
271
378
  """
272
379
  High‑level orchestrator that loads inputs, determines basename/language,
273
380
  computes output locations, and verifies overwrite rules.
381
+
382
+ Supports .pddrc configuration with context-aware settings and configuration hierarchy:
383
+ CLI options > .pddrc context > environment variables > defaults
274
384
 
275
385
  Returns
276
386
  -------
277
- (input_strings, output_file_paths, language)
387
+ (resolved_config, input_strings, output_file_paths, language)
278
388
  """
279
389
  command_options = command_options or {} # Ensure command_options is a dict
280
390
 
391
+ # ------------- Load .pddrc configuration -----------------
392
+ pddrc_config = {}
393
+ context = None
394
+ context_config = {}
395
+
396
+ try:
397
+ # Find and load .pddrc file
398
+ pddrc_path = _find_pddrc_file()
399
+ if pddrc_path:
400
+ pddrc_config = _load_pddrc_config(pddrc_path)
401
+
402
+ # Detect appropriate context
403
+ current_dir = Path.cwd()
404
+ context = _detect_context(current_dir, pddrc_config, context_override)
405
+
406
+ # Get context-specific configuration
407
+ context_config = _get_context_config(pddrc_config, context)
408
+
409
+ if not quiet and context:
410
+ console.print(f"[info]Using .pddrc context:[/info] {context}")
411
+
412
+ # Apply configuration hierarchy
413
+ env_vars = dict(os.environ)
414
+ resolved_config = _resolve_config_hierarchy(command_options, context_config, env_vars)
415
+
416
+ # Update command_options with resolved configuration for internal use
417
+ for key, value in resolved_config.items():
418
+ if key not in command_options or command_options[key] is None:
419
+ command_options[key] = value
420
+
421
+ # Also update context_config with resolved environment variables for generate_output_paths
422
+ # This ensures environment variables are available when context config doesn't override them
423
+ for key, value in resolved_config.items():
424
+ if key.endswith('_output_path') and key not in context_config:
425
+ context_config[key] = value
426
+
427
+ except Exception as e:
428
+ error_msg = f"Configuration error: {e}"
429
+ console.print(f"[error]{error_msg}[/error]", style="error")
430
+ if not quiet:
431
+ console.print("[warning]Continuing with default configuration...[/warning]", style="warning")
432
+ # Initialize resolved_config on error to avoid downstream issues
433
+ resolved_config = command_options.copy()
434
+
435
+
436
+ # ------------- Handle sync discovery mode ----------------
437
+ if command == "sync" and not input_file_paths:
438
+ basename = command_options.get("basename")
439
+ if not basename:
440
+ raise ValueError("Basename must be provided in command_options for sync discovery mode.")
441
+
442
+ # For discovery, we only need directory paths. Call generate_output_paths with dummy values.
443
+ try:
444
+ output_paths_str = generate_output_paths(
445
+ command="sync",
446
+ output_locations={},
447
+ basename=basename,
448
+ language="python", # Dummy language
449
+ file_extension=".py", # Dummy extension
450
+ context_config=context_config,
451
+ )
452
+ # Infer base directories from a sample output path
453
+ gen_path = Path(output_paths_str.get("generate_output_path", "src"))
454
+ resolved_config["prompts_dir"] = str(gen_path.parent.parent / "prompts")
455
+ resolved_config["code_dir"] = str(gen_path.parent)
456
+ resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
457
+ resolved_config["examples_dir"] = str(Path(output_paths_str.get("example_output_path", "examples")).parent)
458
+
459
+ except Exception as e:
460
+ console.print(f"[error]Failed to determine initial paths for sync: {e}", style="error")
461
+ raise
462
+
463
+ # Return early for discovery mode
464
+ return resolved_config, {}, {}, ""
465
+
466
+
281
467
  if not input_file_paths:
282
468
  raise ValueError("No input files provided")
283
469
 
470
+
284
471
  # ------------- normalise & resolve Paths -----------------
285
472
  input_paths: Dict[str, Path] = {}
286
473
  for key, path_str in input_file_paths.items():
@@ -407,6 +594,7 @@ def construct_paths(
407
594
  basename=basename,
408
595
  language=language,
409
596
  file_extension=file_extension,
597
+ context_config=context_config,
410
598
  )
411
599
  # Convert to Path objects for internal use
412
600
  output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
@@ -440,8 +628,12 @@ def construct_paths(
440
628
  click.secho("Operation cancelled.", fg="red", err=True)
441
629
  sys.exit(1) # Exit if user chooses not to overwrite
442
630
  except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
443
- click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
444
- sys.exit(1)
631
+ if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
632
+ # Non-interactive environment, default to not overwriting
633
+ click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
634
+ else:
635
+ click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
636
+ sys.exit(1)
445
637
 
446
638
 
447
639
  # ------------- Final reporting ---------------------------
@@ -462,4 +654,14 @@ def construct_paths(
462
654
  # Since we converted to Path, convert back now.
463
655
  output_file_paths_str_return = {k: str(v) for k, v in output_paths_resolved.items()}
464
656
 
465
- return input_strings, output_file_paths_str_return, language
657
+ # Add resolved paths to the config that gets returned
658
+ resolved_config.update(output_file_paths_str_return)
659
+ # Also add inferred directory paths
660
+ gen_path = Path(resolved_config.get("generate_output_path", "src"))
661
+ resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
662
+ resolved_config["code_dir"] = str(gen_path.parent)
663
+ resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
664
+ resolved_config["examples_dir"] = str(Path(resolved_config.get("example_output_path", "examples")).parent)
665
+
666
+
667
+ return resolved_config, input_strings, output_file_paths_str_return, language
@@ -25,7 +25,7 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
25
25
  command_options = {
26
26
  "output": output
27
27
  }
28
- input_strings, output_file_paths, language = construct_paths(
28
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
29
29
  input_file_paths=input_file_paths,
30
30
  force=ctx.obj.get('force', False),
31
31
  quiet=ctx.obj.get('quiet', False),
@@ -51,22 +51,37 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
51
51
  verbose=ctx.obj.get('verbose', False)
52
52
  )
53
53
 
54
- # Save results
55
- if output_file_paths["output"]:
56
- with open(output_file_paths["output"], 'w') as f:
54
+ # Save results - prioritize orchestration output path over construct_paths result
55
+ final_output_path = output or output_file_paths["output"]
56
+ print(f"DEBUG: output param = {output}")
57
+ print(f"DEBUG: output_file_paths['output'] = {output_file_paths['output']}")
58
+ print(f"DEBUG: final_output_path = {final_output_path}")
59
+ if final_output_path and example_code is not None:
60
+ with open(final_output_path, 'w') as f:
57
61
  f.write(example_code)
62
+ elif final_output_path and example_code is None:
63
+ # Log the error but don't crash
64
+ if not ctx.obj.get('quiet', False):
65
+ rprint("[bold red]Warning:[/bold red] Example generation failed, skipping file write")
58
66
 
59
67
  # Provide user feedback
60
68
  if not ctx.obj.get('quiet', False):
61
- rprint("[bold green]Example code generated successfully.[/bold green]")
62
- rprint(f"[bold]Model used:[/bold] {model_name}")
63
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
64
- if output:
65
- rprint(f"[bold]Example code saved to:[/bold] {output_file_paths['output']}")
69
+ if example_code is not None:
70
+ rprint("[bold green]Example code generated successfully.[/bold green]")
71
+ rprint(f"[bold]Model used:[/bold] {model_name}")
72
+ rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
73
+ if final_output_path and example_code is not None:
74
+ rprint(f"[bold]Example code saved to:[/bold] {final_output_path}")
75
+ else:
76
+ rprint("[bold red]Example code generation failed.[/bold red]")
77
+ rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
66
78
 
67
- # Always print example code, even in quiet mode
68
- rprint("[bold]Generated Example Code:[/bold]")
69
- rprint(example_code)
79
+ # Always print example code, even in quiet mode (if it exists)
80
+ if example_code is not None:
81
+ rprint("[bold]Generated Example Code:[/bold]")
82
+ rprint(example_code)
83
+ else:
84
+ rprint("[bold red]No example code generated due to errors.[/bold red]")
70
85
 
71
86
  return example_code, total_cost, model_name
72
87
 
pdd/crash_main.py CHANGED
@@ -52,15 +52,13 @@ def crash_main(
52
52
  ctx.params = ctx.params if isinstance(ctx.params, dict) else {}
53
53
 
54
54
  quiet = ctx.params.get("quiet", ctx.obj.get("quiet", False))
55
- verbose = ctx.params.get("verbose", ctx.obj.get("verbose", False)) # Get verbose flag
55
+ verbose = ctx.params.get("verbose", ctx.obj.get("verbose", False))
56
56
 
57
- # Get model parameters from context early, including time
58
57
  strength = ctx.obj.get("strength", DEFAULT_STRENGTH)
59
58
  temperature = ctx.obj.get("temperature", 0)
60
- time_param = ctx.obj.get("time", DEFAULT_TIME) # Renamed from time_budget for clarity
59
+ time_param = ctx.obj.get("time", DEFAULT_TIME)
61
60
 
62
61
  try:
63
- # Construct file paths
64
62
  input_file_paths = {
65
63
  "prompt_file": prompt_file,
66
64
  "code_file": code_file,
@@ -73,9 +71,8 @@ def crash_main(
73
71
  }
74
72
 
75
73
  force = ctx.params.get("force", ctx.obj.get("force", False))
76
- # quiet = ctx.params.get("quiet", ctx.obj.get("quiet", False)) # Already defined above
77
74
 
78
- input_strings, output_file_paths, _ = construct_paths(
75
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
79
76
  input_file_paths=input_file_paths,
80
77
  force=force,
81
78
  quiet=quiet,
@@ -83,77 +80,79 @@ def crash_main(
83
80
  command_options=command_options
84
81
  )
85
82
 
86
- # Load input files
87
83
  prompt_content = input_strings["prompt_file"]
88
84
  code_content = input_strings["code_file"]
89
85
  program_content = input_strings["program_file"]
90
86
  error_content = input_strings["error_file"]
91
87
 
92
- # Store original content for comparison later
93
88
  original_code_content = code_content
94
89
  original_program_content = program_content
95
90
 
96
- # Get model parameters from context (strength, temperature, time already fetched)
97
- # strength = ctx.obj.get("strength", DEFAULT_STRENGTH) # Moved up
98
- # temperature = ctx.obj.get("temperature", 0) # Moved up
99
- # time_budget = ctx.obj.get("time", DEFAULT_TIME) # Moved up and renamed
100
-
101
- # verbose = ctx.params.get("verbose", ctx.obj.get("verbose", False)) # Already defined above
102
-
103
91
  code_updated: bool = False
104
92
  program_updated: bool = False
105
93
 
106
94
  if loop:
107
- # Use iterative fixing process
108
- # Corrected parameter order for fix_code_loop, adding time_param
109
95
  success, final_program, final_code, attempts, cost, model = fix_code_loop(
110
96
  code_file, prompt_content, program_file, strength, temperature,
111
97
  max_attempts or 3, budget or 5.0, error_file, verbose, time_param
112
98
  )
113
- # Determine if content was updated by fix_code_loop
114
- if success: # Only consider updates if the loop reported success
115
- code_updated = bool(final_code and final_code != original_code_content)
116
- program_updated = bool(final_program and final_program != original_program_content)
99
+ # Always set final_code/final_program to something non-empty
100
+ if not final_code:
101
+ final_code = original_code_content
102
+ if not final_program:
103
+ final_program = original_program_content
104
+ code_updated = final_code != original_code_content
105
+ program_updated = final_program != original_program_content
117
106
  else:
118
- # Use single fix attempt
119
107
  if fix_code_module_errors is None:
120
- raise ImportError("fix_code_module_errors is required but not available.")
121
- # Corrected parameter order for fix_code_module_errors, adding time_param
122
- fm_update_program, fm_update_code, final_program, final_code, program_code_fix, cost, model = fix_code_module_errors(
123
- program_content, prompt_content, code_content, error_content, strength, temperature, verbose, time_param
108
+ raise ImportError("fix_code_module_errors is required but not available.")
109
+
110
+ update_program, update_code, fixed_program, fixed_code, _, cost, model = fix_code_module_errors(
111
+ program_content, prompt_content, code_content, error_content,
112
+ strength, temperature, verbose, time_param
124
113
  )
125
- success = True # Assume success after one attempt if no exception
114
+ success = True
126
115
  attempts = 1
127
- # Use boolean flags from fix_code_module_errors and ensure content is not empty
128
- code_updated = fm_update_code and bool(final_code)
129
- program_updated = fm_update_program and bool(final_program)
130
116
 
131
- # Removed fallback to original content if final_code/final_program are empty
132
- # An empty string from a fix function means no valid update.
117
+ # Fallback if fixed_program is empty but update_program is True
118
+ if update_program and not fixed_program.strip():
119
+ fixed_program = program_content
120
+ if update_code and not fixed_code.strip():
121
+ fixed_code = code_content
122
+
123
+ final_code = fixed_code if update_code else code_content
124
+ final_program = fixed_program if update_program else program_content
125
+
126
+ # Always set final_code/final_program to something non-empty
127
+ if not final_code:
128
+ final_code = original_code_content
129
+ if not final_program:
130
+ final_program = original_program_content
131
+
132
+ code_updated = final_code != original_code_content
133
+ program_updated = final_program != original_program_content
133
134
 
134
- # Determine whether to write the files based on whether paths are provided AND content was updated
135
135
  output_code_path_str = output_file_paths.get("output")
136
136
  output_program_path_str = output_file_paths.get("output_program")
137
137
 
138
- # Write output files only if updated and path provided
139
- if output_code_path_str and code_updated:
138
+ # Always write output files if output paths are specified
139
+ if output_code_path_str:
140
140
  output_code_path = Path(output_code_path_str)
141
- output_code_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
141
+ output_code_path.parent.mkdir(parents=True, exist_ok=True)
142
142
  with open(output_code_path, "w") as f:
143
143
  f.write(final_code)
144
144
 
145
- if output_program_path_str and program_updated:
145
+ if output_program_path_str:
146
146
  output_program_path = Path(output_program_path_str)
147
- output_program_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
147
+ output_program_path.parent.mkdir(parents=True, exist_ok=True)
148
148
  with open(output_program_path, "w") as f:
149
149
  f.write(final_program)
150
150
 
151
- # Provide user feedback
152
151
  if not quiet:
153
152
  if success:
154
- rprint("[bold green]Crash fix attempt completed.[/bold green]") # Changed message slightly
153
+ rprint("[bold green]Crash fix attempt completed.[/bold green]")
155
154
  else:
156
- rprint("[bold yellow]Crash fix attempt completed with issues or no changes made.[/bold yellow]") # Changed message
155
+ rprint("[bold yellow]Crash fix attempt completed with issues.[/bold yellow]")
157
156
  rprint(f"[bold]Model used:[/bold] {model}")
158
157
  rprint(f"[bold]Total attempts:[/bold] {attempts}")
159
158
  rprint(f"[bold]Total cost:[/bold] ${cost:.2f}")
@@ -162,25 +161,20 @@ def crash_main(
162
161
  if code_updated:
163
162
  rprint(f"[bold]Fixed code saved to:[/bold] {output_code_path_str}")
164
163
  else:
165
- rprint(f"[info]Code file {Path(code_file).name} was not modified. Output file {output_code_path_str} not written.[/info]")
166
-
164
+ rprint(f"[info]Code file '{Path(code_file).name}' was not modified (but output file was written).[/info]")
167
165
  if output_program_path_str:
168
166
  if program_updated:
169
167
  rprint(f"[bold]Fixed program saved to:[/bold] {output_program_path_str}")
170
168
  else:
171
- rprint(f"[info]Program file {Path(program_file).name} was not modified. Output file {output_program_path_str} not written.[/info]")
169
+ rprint(f"[info]Program file '{Path(program_file).name}' was not modified (but output file was written).[/info]")
172
170
 
173
171
  return success, final_code, final_program, attempts, cost, model
174
-
172
+
175
173
  except FileNotFoundError as e:
176
174
  if not quiet:
177
- # Provide a more specific error message for file not found
178
- rprint(f"[bold red]Error:[/bold red] Input file not found: {e}")
175
+ rprint(f"[bold red]Error:[/bold red] Input file not found: {e}")
179
176
  sys.exit(1)
180
177
  except Exception as e:
181
178
  if not quiet:
182
179
  rprint(f"[bold red]An unexpected error occurred:[/bold red] {str(e)}")
183
- # Consider logging the full traceback here for debugging
184
- # import traceback
185
- # traceback.print_exc()
186
180
  sys.exit(1)
pdd/data/llm_model.csv CHANGED
@@ -6,7 +6,7 @@ OpenAI,deepseek/deepseek-chat,.27,1.1,1353,https://api.deepseek.com/beta,DEEPSEE
6
6
  Google,vertex_ai/gemini-2.5-flash-preview-04-17,0.15,0.6,1330,,VERTEX_CREDENTIALS,0,True,effort
7
7
  Google,gemini-2.5-pro-exp-03-25,1.25,10.0,1360,,GOOGLE_API_KEY,0,True,none
8
8
  Anthropic,claude-sonnet-4-20250514,3.0,15.0,1340,,ANTHROPIC_API_KEY,64000,True,budget
9
- Google,vertex_ai/gemini-2.5-pro-preview-05-06,1.25,10.0,1361,,VERTEX_CREDENTIALS,0,True,none
9
+ Google,vertex_ai/gemini-2.5-pro,1.25,10.0,1361,,VERTEX_CREDENTIALS,0,True,none
10
10
  OpenAI,o4-mini,1.1,4.4,1333,,OPENAI_API_KEY,0,True,effort
11
11
  OpenAI,o3,10.0,40.0,1389,,OPENAI_API_KEY,0,True,effort
12
12
  OpenAI,gpt-4.1,2.0,8.0,1335,,OPENAI_API_KEY,0,True,none
pdd/detect_change_main.py CHANGED
@@ -39,7 +39,7 @@ def detect_change_main(
39
39
  "output": output
40
40
  }
41
41
 
42
- input_strings, output_file_paths, _ = construct_paths(
42
+ resolved_config, input_strings, output_file_paths, _ = construct_paths(
43
43
  input_file_paths=input_file_paths,
44
44
  force=ctx.obj.get('force', False),
45
45
  quiet=ctx.obj.get('quiet', False),
@@ -81,6 +81,18 @@ def fix_code_module_errors(
81
81
  model_name = first_response.get('model_name', '')
82
82
  program_code_fix = first_response['result']
83
83
 
84
+ # Check if the LLM response is None or an error string
85
+ if program_code_fix is None:
86
+ error_msg = "LLM returned None result during error analysis"
87
+ if verbose:
88
+ print(f"[red]{error_msg}[/red]")
89
+ raise RuntimeError(error_msg)
90
+ elif isinstance(program_code_fix, str) and program_code_fix.startswith("ERROR:"):
91
+ error_msg = f"LLM failed to analyze errors: {program_code_fix}"
92
+ if verbose:
93
+ print(f"[red]{error_msg}[/red]")
94
+ raise RuntimeError(error_msg)
95
+
84
96
  if verbose:
85
97
  print("[green]Error analysis complete[/green]")
86
98
  print(Markdown(program_code_fix))
pdd/fix_main.py CHANGED
@@ -91,7 +91,7 @@ def fix_main(
91
91
  "output_results": output_results
92
92
  }
93
93
 
94
- input_strings, output_file_paths, _ = construct_paths(
94
+ resolved_config, input_strings, output_file_paths, _ = construct_paths(
95
95
  input_file_paths=input_file_paths,
96
96
  force=ctx.obj.get('force', False),
97
97
  quiet=ctx.obj.get('quiet', False),
@@ -177,7 +177,7 @@ def fix_main(
177
177
  try:
178
178
  # Get JWT token for cloud authentication
179
179
  jwt_token = asyncio.run(get_jwt_token(
180
- firebase_api_key=os.environ.get("REACT_APP_FIREBASE_API_KEY"),
180
+ firebase_api_key=os.environ.get("NEXT_PUBLIC_FIREBASE_API_KEY"),
181
181
  github_client_id=os.environ.get("GITHUB_CLIENT_ID"),
182
182
  app_name="PDD Code Generator"
183
183
  ))
@@ -268,6 +268,13 @@ def fix_verification_errors(
268
268
  fixed_program = fix_result_obj.fixed_program
269
269
  fixed_code = fix_result_obj.fixed_code
270
270
  fix_explanation = fix_result_obj.explanation
271
+
272
+ # Unescape literal \n strings to actual newlines
273
+ if fixed_program:
274
+ fixed_program = fixed_program.replace('\\n', '\n')
275
+ if fixed_code:
276
+ fixed_code = fixed_code.replace('\\n', '\n')
277
+
271
278
  parsed_fix_successfully = True
272
279
  if verbose:
273
280
  rprint("[green]Successfully parsed structured output for fix.[/green]")
@@ -282,6 +289,12 @@ def fix_verification_errors(
282
289
  fixed_code_candidate = code_match.group(1).strip() if (code_match and code_match.group(1)) else None
283
290
  fix_explanation_candidate = explanation_match.group(1).strip() if (explanation_match and explanation_match.group(1)) else None
284
291
 
292
+ # Unescape literal \n strings to actual newlines
293
+ if fixed_program_candidate:
294
+ fixed_program_candidate = fixed_program_candidate.replace('\\n', '\n')
295
+ if fixed_code_candidate:
296
+ fixed_code_candidate = fixed_code_candidate.replace('\\n', '\n')
297
+
285
298
  fixed_program = fixed_program_candidate if fixed_program_candidate else program
286
299
  fixed_code = fixed_code_candidate if fixed_code_candidate else code
287
300
  fix_explanation = fix_explanation_candidate if fix_explanation_candidate else "[Fix explanation not provided by LLM]"
@@ -183,7 +183,7 @@ def fix_verification_main(
183
183
 
184
184
  try:
185
185
  # First try the official helper.
186
- input_strings, output_file_paths, language = construct_paths(
186
+ resolved_config, input_strings, output_file_paths, language = construct_paths(
187
187
  input_file_paths=input_file_paths,
188
188
  force=force,
189
189
  quiet=quiet,
@@ -382,7 +382,7 @@ def fix_verification_main(
382
382
  if final_code is not None:
383
383
  rich_print(f" len(final_code): {len(final_code)}")
384
384
 
385
- if success and output_code_path and final_code is not None:
385
+ if output_code_path and final_code is not None:
386
386
  try:
387
387
  if verbose:
388
388
  rich_print(f"[cyan bold DEBUG] In fix_verification_main, ATTEMPTING to write code to: {output_code_path!r}")
@@ -402,7 +402,7 @@ def fix_verification_main(
402
402
  if final_program is not None:
403
403
  rich_print(f" len(final_program): {len(final_program)}")
404
404
 
405
- if success and output_program_path and final_program is not None:
405
+ if output_program_path and final_program is not None:
406
406
  try:
407
407
  if verbose:
408
408
  rich_print(f"[cyan bold DEBUG] In fix_verification_main, ATTEMPTING to write program to: {output_program_path!r}")