pdd-cli 0.0.45__py3-none-any.whl → 0.0.90__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 (114) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +73 -21
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +258 -82
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -63
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +330 -76
  43. pdd/fix_error_loop.py +207 -61
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +306 -272
  48. pdd/fix_verification_main.py +28 -9
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +9 -2
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/incremental_code_generator.py +2 -2
  56. pdd/insert_includes.py +11 -3
  57. pdd/llm_invoke.py +1269 -103
  58. pdd/load_prompt_template.py +36 -10
  59. pdd/pdd_completion.fish +25 -2
  60. pdd/pdd_completion.sh +30 -4
  61. pdd/pdd_completion.zsh +79 -4
  62. pdd/postprocess.py +10 -3
  63. pdd/preprocess.py +228 -15
  64. pdd/preprocess_main.py +8 -5
  65. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  66. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  67. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  68. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  69. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  70. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  71. pdd/prompts/auto_include_LLM.prompt +100 -905
  72. pdd/prompts/detect_change_LLM.prompt +122 -20
  73. pdd/prompts/example_generator_LLM.prompt +22 -1
  74. pdd/prompts/extract_code_LLM.prompt +5 -1
  75. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  76. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  77. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  78. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  79. pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
  80. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
  81. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  82. pdd/prompts/generate_test_LLM.prompt +21 -6
  83. pdd/prompts/increase_tests_LLM.prompt +1 -5
  84. pdd/prompts/insert_includes_LLM.prompt +228 -108
  85. pdd/prompts/trace_LLM.prompt +25 -22
  86. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  87. pdd/prompts/update_prompt_LLM.prompt +22 -1
  88. pdd/pytest_output.py +127 -12
  89. pdd/render_mermaid.py +236 -0
  90. pdd/setup_tool.py +648 -0
  91. pdd/simple_math.py +2 -0
  92. pdd/split_main.py +3 -2
  93. pdd/summarize_directory.py +49 -6
  94. pdd/sync_determine_operation.py +543 -98
  95. pdd/sync_main.py +81 -31
  96. pdd/sync_orchestration.py +1334 -751
  97. pdd/sync_tui.py +848 -0
  98. pdd/template_registry.py +264 -0
  99. pdd/templates/architecture/architecture_json.prompt +242 -0
  100. pdd/templates/generic/generate_prompt.prompt +174 -0
  101. pdd/trace.py +168 -12
  102. pdd/trace_main.py +4 -3
  103. pdd/track_cost.py +151 -61
  104. pdd/unfinished_prompt.py +49 -3
  105. pdd/update_main.py +549 -67
  106. pdd/update_model_costs.py +2 -2
  107. pdd/update_prompt.py +19 -4
  108. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
  109. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  110. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  111. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  112. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  113. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  114. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/construct_paths.py CHANGED
@@ -4,7 +4,7 @@ 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, List
7
+ from typing import Dict, Tuple, Any, Optional, List, Callable
8
8
  import fnmatch
9
9
  import logging
10
10
 
@@ -56,6 +56,23 @@ def _load_pddrc_config(pddrc_path: Path) -> Dict[str, Any]:
56
56
  except Exception as e:
57
57
  raise ValueError(f"Error loading .pddrc: {e}")
58
58
 
59
+ def list_available_contexts(start_path: Optional[Path] = None) -> list[str]:
60
+ """Return sorted context names from the nearest .pddrc.
61
+
62
+ - Searches upward from `start_path` (or CWD) for a `.pddrc` file.
63
+ - If found, loads and validates it, then returns sorted context names.
64
+ - If no `.pddrc` exists, returns ["default"].
65
+ - Propagates ValueError for malformed `.pddrc` to allow callers to render
66
+ helpful errors.
67
+ """
68
+ pddrc = _find_pddrc_file(start_path)
69
+ if not pddrc:
70
+ return ["default"]
71
+ config = _load_pddrc_config(pddrc)
72
+ contexts = config.get("contexts", {})
73
+ names = sorted(contexts.keys()) if isinstance(contexts, dict) else []
74
+ return names or ["default"]
75
+
59
76
  def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
60
77
  """Detect the appropriate context based on current directory path."""
61
78
  if context_override:
@@ -108,8 +125,9 @@ def _resolve_config_hierarchy(
108
125
  # Configuration keys to resolve
109
126
  config_keys = {
110
127
  'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
111
- 'test_output_path': 'PDD_TEST_OUTPUT_PATH',
128
+ 'test_output_path': 'PDD_TEST_OUTPUT_PATH',
112
129
  'example_output_path': 'PDD_EXAMPLE_OUTPUT_PATH',
130
+ 'prompts_dir': 'PDD_PROMPTS_DIR',
113
131
  'default_language': 'PDD_DEFAULT_LANGUAGE',
114
132
  'target_coverage': 'PDD_TEST_COVERAGE_TARGET',
115
133
  'strength': None,
@@ -176,52 +194,41 @@ def _candidate_prompt_path(input_files: Dict[str, Path]) -> Path | None:
176
194
  for p in input_files.values():
177
195
  if p.suffix == ".prompt":
178
196
  return p
197
+
198
+ # Final fallback: Return the first file path available (e.g. for pdd update <code_file>)
199
+ if input_files:
200
+ return next(iter(input_files.values()))
201
+
179
202
  return None
180
203
 
181
204
 
182
205
  # New helper function to check if a language is known
183
206
  def _is_known_language(language_name: str) -> bool:
184
- """Checks if a language name is present in the language_format.csv."""
207
+ """Return True if the language is recognized.
208
+
209
+ Prefer CSV in PDD_PATH if available; otherwise fall back to a built-in set
210
+ so basename/language inference does not fail when PDD_PATH is unset.
211
+ """
212
+ language_name_lower = (language_name or "").lower()
213
+ if not language_name_lower:
214
+ return False
215
+
216
+ builtin_languages = {
217
+ 'python', 'javascript', 'typescript', 'java', 'cpp', 'c', 'go', 'ruby', 'rust',
218
+ 'kotlin', 'swift', 'csharp', 'php', 'scala', 'r', 'lua', 'perl', 'bash', 'shell',
219
+ 'powershell', 'sql', 'prompt', 'html', 'css', 'makefile',
220
+ # Common data and config formats for architecture prompts and configs
221
+ 'json', 'jsonl', 'yaml', 'yml', 'toml', 'ini'
222
+ }
223
+
185
224
  pdd_path_str = os.getenv('PDD_PATH')
186
225
  if not pdd_path_str:
187
- # Consistent with get_extension, raise ValueError if PDD_PATH is not set.
188
- # Or, for an internal helper, we might decide to log and return False,
189
- # but raising an error for missing config is generally safer.
190
- # However, _determine_language (the caller) already raises ValueError
191
- # if language cannot be found, so this path might not be strictly necessary
192
- # if we assume PDD_PATH is validated earlier or by other get_extension/get_language calls.
193
- # For robustness here, let's keep a check but perhaps make it less severe if called internally.
194
- # For now, align with how get_extension might handle it.
195
- # console.print("[error]PDD_PATH environment variable is not set. Cannot validate language.", style="error")
196
- # return False # Or raise error
197
- # Given this is internal and other functions (get_extension) already depend on PDD_PATH,
198
- # we can assume if those ran, PDD_PATH is set. If not, they'd fail first.
199
- # So, we can simplify or rely on that pre-condition.
200
- # Let's assume PDD_PATH will be set if other language functions are working.
201
- # If it's critical, an explicit check and raise ValueError is better.
202
- # For now, let's proceed assuming PDD_PATH is available if this point is reached.
203
- pass # Assuming PDD_PATH is checked by get_extension/get_language if they are called
204
-
205
- # If PDD_PATH is not set, this will likely fail earlier if get_extension/get_language are used.
206
- # If we want this helper to be fully independent, it needs robust PDD_PATH handling.
207
- # Let's assume for now, PDD_PATH is available if this point is reached through normal flow.
208
-
209
- # Re-evaluate: PDD_PATH is critical for this function. Let's keep the check.
210
- if not pdd_path_str:
211
- # This helper might be called before get_extension in some logic paths
212
- # if _determine_language prioritizes suffix checking first.
213
- # So, it needs its own PDD_PATH check.
214
- # Raise ValueError to be consistent with get_extension's behavior.
215
- raise ValueError("PDD_PATH environment variable is not set. Cannot validate language.")
226
+ return language_name_lower in builtin_languages
216
227
 
217
228
  csv_file_path = Path(pdd_path_str) / 'data' / 'language_format.csv'
218
-
219
229
  if not csv_file_path.is_file():
220
- # Raise FileNotFoundError if CSV is missing, consistent with get_extension
221
- raise FileNotFoundError(f"The language format CSV file does not exist: {csv_file_path}")
222
-
223
- language_name_lower = language_name.lower()
224
-
230
+ return language_name_lower in builtin_languages
231
+
225
232
  try:
226
233
  with open(csv_file_path, mode='r', encoding='utf-8', newline='') as csvfile:
227
234
  reader = csv.DictReader(csvfile)
@@ -229,10 +236,10 @@ def _is_known_language(language_name: str) -> bool:
229
236
  if row.get('language', '').lower() == language_name_lower:
230
237
  return True
231
238
  except csv.Error as e:
232
- # Log and return False or raise a custom error
233
239
  console.print(f"[error]CSV Error reading {csv_file_path}: {e}", style="error")
234
- return False # Indicates language could not be confirmed due to CSV issue
235
- return False
240
+ return language_name_lower in builtin_languages
241
+
242
+ return language_name_lower in builtin_languages
236
243
 
237
244
 
238
245
  def _strip_language_suffix(path_like: os.PathLike[str]) -> str:
@@ -264,6 +271,24 @@ def _extract_basename(
264
271
  """
265
272
  Deduce the project basename according to the rules explained in *Step A*.
266
273
  """
274
+ # Handle 'fix' command specifically to create a unique basename per test file
275
+ if command == "fix":
276
+ prompt_path = _candidate_prompt_path(input_file_paths)
277
+ if not prompt_path:
278
+ raise ValueError("Could not determine prompt file for 'fix' command.")
279
+
280
+ prompt_basename = _strip_language_suffix(prompt_path)
281
+
282
+ unit_test_path = input_file_paths.get("unit_test_file")
283
+ if not unit_test_path:
284
+ # Fallback to just the prompt basename if no unit test file is provided
285
+ # This might happen in some edge cases, but 'fix' command structure requires it
286
+ return prompt_basename
287
+
288
+ # Use the stem of the unit test file to make the basename unique
289
+ test_basename = Path(unit_test_path).stem
290
+ return f"{prompt_basename}_{test_basename}"
291
+
267
292
  # Handle conflicts first due to its unique structure
268
293
  if command == "conflicts":
269
294
  key1 = "prompt1"
@@ -331,14 +356,48 @@ def _determine_language(
331
356
  ext = path_obj.suffix
332
357
  # Prioritize non-prompt code files
333
358
  if ext and ext != ".prompt":
334
- language = get_language(ext)
335
- if language:
336
- return language.lower()
359
+ try:
360
+ language = get_language(ext)
361
+ if language:
362
+ return language.lower()
363
+ except ValueError:
364
+ # Fallback: load language CSV file directly when PDD_PATH is not set
365
+ try:
366
+ import csv
367
+ import os
368
+ # Try to find the CSV file relative to this script
369
+ script_dir = os.path.dirname(os.path.abspath(__file__))
370
+ csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
371
+ if os.path.exists(csv_path):
372
+ with open(csv_path, 'r') as csvfile:
373
+ reader = csv.DictReader(csvfile)
374
+ for row in reader:
375
+ if row['extension'].lower() == ext.lower():
376
+ return row['language'].lower()
377
+ except (FileNotFoundError, csv.Error):
378
+ pass
337
379
  # Handle files without extension like Makefile
338
380
  elif not ext and path_obj.is_file(): # Check it's actually a file
339
- language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
340
- if language:
341
- return language.lower()
381
+ try:
382
+ language = get_language(path_obj.name) # Check name (e.g., 'Makefile')
383
+ if language:
384
+ return language.lower()
385
+ except ValueError:
386
+ # Fallback: load language CSV file directly for files without extension
387
+ try:
388
+ import csv
389
+ import os
390
+ script_dir = os.path.dirname(os.path.abspath(__file__))
391
+ csv_path = os.path.join(script_dir, 'data', 'language_format.csv')
392
+ if os.path.exists(csv_path):
393
+ with open(csv_path, 'r') as csvfile:
394
+ reader = csv.DictReader(csvfile)
395
+ for row in reader:
396
+ # Check if the filename matches (for files without extension)
397
+ if not row['extension'] and path_obj.name.lower() == row['language'].lower():
398
+ return row['language'].lower()
399
+ except (FileNotFoundError, csv.Error):
400
+ pass
342
401
 
343
402
  # 3 – parse from prompt filename suffix
344
403
  prompt_path = _candidate_prompt_path(input_file_paths)
@@ -354,7 +413,7 @@ def _determine_language(
354
413
 
355
414
  # 4 - Special handling for detect command - default to prompt for LLM prompts
356
415
  if command == "detect" and "change_file" in input_file_paths:
357
- return "prompt" # Default to prompt for detect command
416
+ return "prompt"
358
417
 
359
418
  # 5 - If no language determined, raise error
360
419
  raise ValueError("Could not determine language from input files or options.")
@@ -374,6 +433,8 @@ def construct_paths(
374
433
  command_options: Optional[Dict[str, Any]], # Allow None
375
434
  create_error_file: bool = True, # Added parameter to control error file creation
376
435
  context_override: Optional[str] = None, # Added parameter for context override
436
+ confirm_callback: Optional[Callable[[str, str], bool]] = None, # Callback for interactive confirmation
437
+ path_resolution_mode: Optional[str] = None, # "cwd" or "config_base" - if None, use command default
377
438
  ) -> Tuple[Dict[str, Any], Dict[str, str], Dict[str, str], str]:
378
439
  """
379
440
  High‑level orchestrator that loads inputs, determines basename/language,
@@ -390,6 +451,7 @@ def construct_paths(
390
451
 
391
452
  # ------------- Load .pddrc configuration -----------------
392
453
  pddrc_config = {}
454
+ pddrc_path: Optional[Path] = None
393
455
  context = None
394
456
  context_config = {}
395
457
  original_context_config = {} # Keep track of original context config for sync discovery
@@ -450,7 +512,10 @@ def construct_paths(
450
512
  language="python", # Dummy language
451
513
  file_extension=".py", # Dummy extension
452
514
  context_config=context_config,
515
+ config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
516
+ path_resolution_mode="cwd", # Sync resolves paths relative to CWD
453
517
  )
518
+
454
519
  # Infer base directories from a sample output path
455
520
  gen_path = Path(output_paths_str.get("generate_output_path", "src"))
456
521
 
@@ -467,9 +532,9 @@ def construct_paths(
467
532
  # Fall back to context-aware logic
468
533
  # Use original_context_config to avoid checking augmented config with env vars
469
534
  if original_context_config and any(key.endswith('_output_path') for key in original_context_config):
470
- # For configured contexts, prompts are typically at the same level as output dirs
471
- # e.g., if code goes to "pdd/", prompts should be at "prompts/" (siblings)
472
- resolved_config["prompts_dir"] = "prompts"
535
+ # For configured contexts, use prompts_dir from config if provided,
536
+ # otherwise default to "prompts" at the same level as output dirs
537
+ resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
473
538
  resolved_config["code_dir"] = str(gen_path.parent)
474
539
  else:
475
540
  # For default contexts, maintain relative relationship
@@ -478,7 +543,14 @@ def construct_paths(
478
543
  resolved_config["code_dir"] = str(gen_path.parent)
479
544
 
480
545
  resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
481
- resolved_config["examples_dir"] = str(Path(output_paths_str.get("example_output_path", "examples")).parent)
546
+ # example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
547
+ # If it ends with / or has no file extension, treat as directory; otherwise use parent
548
+ example_path_str = output_paths_str.get("example_output_path", "examples")
549
+ example_path = Path(example_path_str)
550
+ if example_path_str.endswith('/') or '.' not in example_path.name:
551
+ resolved_config["examples_dir"] = example_path_str.rstrip('/')
552
+ else:
553
+ resolved_config["examples_dir"] = str(example_path.parent)
482
554
 
483
555
  except Exception as e:
484
556
  console.print(f"[error]Failed to determine initial paths for sync: {e}", style="error")
@@ -497,11 +569,15 @@ def construct_paths(
497
569
  for key, path_str in input_file_paths.items():
498
570
  try:
499
571
  path = Path(path_str).expanduser()
500
- # Resolve non-error files strictly first
572
+ # Resolve non-error files strictly first, but be more lenient for sync command
501
573
  if key != "error_file":
502
- # Let FileNotFoundError propagate naturally if path doesn't exist
503
- resolved_path = path.resolve(strict=True)
504
- input_paths[key] = resolved_path
574
+ # For sync command, be more tolerant of non-existent files since we're just determining paths
575
+ if command == "sync":
576
+ input_paths[key] = path.resolve()
577
+ else:
578
+ # Let FileNotFoundError propagate naturally if path doesn't exist
579
+ resolved_path = path.resolve(strict=True)
580
+ input_paths[key] = resolved_path
505
581
  else:
506
582
  # Resolve error file non-strictly, existence checked later
507
583
  input_paths[key] = path.resolve()
@@ -531,9 +607,14 @@ def construct_paths(
531
607
 
532
608
  # Check existence again, especially for error_file which might have been created
533
609
  if not path.exists():
534
- # This case should ideally be caught by resolve(strict=True) earlier for non-error files
535
- # Raise standard FileNotFoundError
536
- raise FileNotFoundError(f"{path}")
610
+ # For sync command, be more tolerant of non-existent files since we're just determining paths
611
+ if command == "sync":
612
+ # Skip reading content for non-existent files in sync mode
613
+ continue
614
+ else:
615
+ # This case should ideally be caught by resolve(strict=True) earlier for non-error files
616
+ # Raise standard FileNotFoundError
617
+ raise FileNotFoundError(f"{path}")
537
618
 
538
619
  if path.is_file(): # Read only if it's a file
539
620
  try:
@@ -598,7 +679,25 @@ def construct_paths(
598
679
  style="warning"
599
680
  )
600
681
 
601
- file_extension = get_extension(language) # Pass determined language
682
+
683
+ # Try to get extension from CSV; fallback to built-in mapping if PDD_PATH/CSV unavailable
684
+ try:
685
+ file_extension = get_extension(language) # Pass determined language
686
+ if not file_extension and (language or '').lower() != 'prompt':
687
+ raise ValueError('empty extension')
688
+ except Exception:
689
+ builtin_ext_map = {
690
+ 'python': '.py', 'javascript': '.js', 'typescript': '.ts', 'java': '.java',
691
+ 'cpp': '.cpp', 'c': '.c', 'go': '.go', 'ruby': '.rb', 'rust': '.rs',
692
+ 'kotlin': '.kt', 'swift': '.swift', 'csharp': '.cs', 'php': '.php',
693
+ 'scala': '.scala', 'r': '.r', 'lua': '.lua', 'perl': '.pl', 'bash': '.sh',
694
+ 'shell': '.sh', 'powershell': '.ps1', 'sql': '.sql', 'html': '.html', 'css': '.css',
695
+ 'prompt': '.prompt', 'makefile': '',
696
+ # Common data/config formats
697
+ 'json': '.json', 'jsonl': '.jsonl', 'yaml': '.yaml', 'yml': '.yml', 'toml': '.toml', 'ini': '.ini'
698
+ }
699
+ file_extension = builtin_ext_map.get(language.lower(), f".{language.lower()}" if language else '')
700
+
602
701
 
603
702
 
604
703
  # ------------- Step 3b: build output paths ---------------
@@ -608,10 +707,52 @@ def construct_paths(
608
707
  if k.startswith("output") and v is not None # Ensure value is not None
609
708
  }
610
709
 
710
+ # Determine input file directory for default output path generation
711
+ # Only apply for commands that generate/update files based on specific input files
712
+ # Commands like sync, generate, test, example have their own directory management
713
+ commands_using_input_dir = {'fix', 'crash', 'verify', 'split', 'change', 'update'}
714
+ input_file_dir: Optional[str] = None
715
+ input_file_dirs: Dict[str, Optional[str]] = {}
716
+ if input_paths and command in commands_using_input_dir:
717
+ try:
718
+ # For fix/crash/verify commands, use specific file directories for each output
719
+ if command in {'fix', 'crash', 'verify'}:
720
+ # Map output keys to their corresponding input file keys
721
+ input_key_map = {
722
+ 'fix': {'output_code': 'code_file', 'output_test': 'unit_test_file', 'output_results': 'code_file'},
723
+ 'crash': {'output': 'code_file', 'output_program': 'program_file'},
724
+ 'verify': {'output_code': 'code_file', 'output_program': 'verification_program', 'output_results': 'code_file'},
725
+ }
726
+
727
+ for output_key, input_key in input_key_map.get(command, {}).items():
728
+ if input_key in input_paths:
729
+ input_file_dirs[output_key] = str(input_paths[input_key].parent)
730
+
731
+ # Set default input_file_dir to code_file directory as fallback
732
+ if 'code_file' in input_paths:
733
+ input_file_dir = str(input_paths['code_file'].parent)
734
+ else:
735
+ first_input_path = next(iter(input_paths.values()))
736
+ input_file_dir = str(first_input_path.parent)
737
+ else:
738
+ # For other commands, use first input path
739
+ first_input_path = next(iter(input_paths.values()))
740
+ input_file_dir = str(first_input_path.parent)
741
+ except (StopIteration, AttributeError):
742
+ # If no input paths or path doesn't have parent, use None (falls back to CWD)
743
+ pass
744
+
611
745
  try:
612
746
  # generate_output_paths might return Dict[str, str] or Dict[str, Path]
613
747
  # Let's assume it returns Dict[str, str] based on verification error,
614
748
  # and convert them to Path objects here.
749
+ # Determine path resolution mode:
750
+ # - If explicitly provided, use it
751
+ # - Otherwise: sync uses "cwd", other commands use "config_base"
752
+ effective_path_resolution_mode = path_resolution_mode
753
+ if effective_path_resolution_mode is None:
754
+ effective_path_resolution_mode = "cwd" if command == "sync" else "config_base"
755
+
615
756
  output_paths_str: Dict[str, str] = generate_output_paths(
616
757
  command=command,
617
758
  output_locations=output_location_opts,
@@ -619,7 +760,12 @@ def construct_paths(
619
760
  language=language,
620
761
  file_extension=file_extension,
621
762
  context_config=context_config,
763
+ input_file_dir=input_file_dir,
764
+ input_file_dirs=input_file_dirs,
765
+ config_base_dir=str(pddrc_path.parent) if pddrc_path else None,
766
+ path_resolution_mode=effective_path_resolution_mode,
622
767
  )
768
+
623
769
  # Convert to Path objects for internal use
624
770
  output_paths_resolved: Dict[str, Path] = {k: Path(v) for k, v in output_paths_str.items()}
625
771
 
@@ -628,36 +774,59 @@ def construct_paths(
628
774
  raise # Re-raise the ValueError
629
775
 
630
776
  # ------------- Step 4: overwrite confirmation ------------
631
- # Check if any output *file* exists (operate on Path objects)
777
+ # Initialize existing_files before the conditional to avoid UnboundLocalError
632
778
  existing_files: Dict[str, Path] = {}
633
- for k, p_obj in output_paths_resolved.items():
634
- # p_obj = Path(p_val) # Conversion now happens earlier
635
- if p_obj.is_file():
636
- existing_files[k] = p_obj # Store the Path object
779
+
780
+ if command in ["test", "bug"] and not force:
781
+ # For test/bug commands without --force, create numbered files instead of overwriting
782
+ for key, path in output_paths_resolved.items():
783
+ if path.is_file():
784
+ base, ext = os.path.splitext(path)
785
+ i = 1
786
+ new_path = Path(f"{base}_{i}{ext}")
787
+ while new_path.exists():
788
+ i += 1
789
+ new_path = Path(f"{base}_{i}{ext}")
790
+ output_paths_resolved[key] = new_path
791
+ else:
792
+ # Check if any output *file* exists (operate on Path objects)
793
+ for k, p_obj in output_paths_resolved.items():
794
+ if p_obj.is_file():
795
+ existing_files[k] = p_obj # Store the Path object
637
796
 
638
797
  if existing_files and not force:
798
+ paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
639
799
  if not quiet:
640
800
  # Use the Path objects stored in existing_files for resolve()
641
801
  # Print without Rich tags for easier testing
642
- paths_list = "\n".join(f" • {p.resolve()}" for p in existing_files.values())
643
802
  console.print(
644
803
  f"Warning: The following output files already exist and may be overwritten:\n{paths_list}",
645
804
  style="warning"
646
805
  )
647
- # Use click.confirm for user interaction
648
- try:
649
- if not click.confirm(
650
- click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
651
- ):
652
- click.secho("Operation cancelled.", fg="red", err=True)
653
- sys.exit(1) # Exit if user chooses not to overwrite
654
- except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
655
- if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
656
- # Non-interactive environment, default to not overwriting
657
- click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
658
- else:
659
- click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
660
- sys.exit(1)
806
+
807
+ # Use confirm_callback if provided (for TUI environments), otherwise use click.confirm
808
+ if confirm_callback is not None:
809
+ # Use the provided callback for confirmation (e.g., from Textual TUI)
810
+ confirm_message = f"The following files will be overwritten:\n{paths_list}\n\nOverwrite existing files?"
811
+ if not confirm_callback(confirm_message, "Overwrite Confirmation"):
812
+ raise click.Abort()
813
+ else:
814
+ # Use click.confirm for CLI interaction
815
+ try:
816
+ if not click.confirm(
817
+ click.style("Overwrite existing files?", fg="yellow"), default=True, show_default=True
818
+ ):
819
+ click.secho("Operation cancelled.", fg="red", err=True)
820
+ raise click.Abort()
821
+ except click.Abort:
822
+ raise # Let Abort propagate to be handled by PDDCLI.invoke()
823
+ except Exception as e: # Catch potential errors during confirm (like EOFError in non-interactive)
824
+ if 'EOF' in str(e) or 'end-of-file' in str(e).lower():
825
+ # Non-interactive environment, default to not overwriting
826
+ click.secho("Non-interactive environment detected. Use --force to overwrite existing files.", fg="yellow", err=True)
827
+ else:
828
+ click.secho(f"Confirmation failed: {e}. Aborting.", fg="red", err=True)
829
+ raise click.Abort()
661
830
 
662
831
 
663
832
  # ------------- Final reporting ---------------------------
@@ -685,7 +854,14 @@ def construct_paths(
685
854
  resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
686
855
  resolved_config["code_dir"] = str(gen_path.parent)
687
856
  resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
688
- resolved_config["examples_dir"] = str(Path(resolved_config.get("example_output_path", "examples")).parent)
857
+ # example_output_path can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
858
+ # If it ends with / or has no file extension, treat as directory; otherwise use parent
859
+ example_path_str = resolved_config.get("example_output_path", "examples")
860
+ example_path = Path(example_path_str)
861
+ if example_path_str.endswith('/') or '.' not in example_path.name:
862
+ resolved_config["examples_dir"] = example_path_str.rstrip('/')
863
+ else:
864
+ resolved_config["examples_dir"] = str(example_path.parent)
689
865
 
690
866
 
691
- return resolved_config, input_strings, output_file_paths_str_return, language
867
+ return resolved_config, input_strings, output_file_paths_str_return, language
pdd/context_generator.py CHANGED
@@ -16,6 +16,9 @@ def context_generator(
16
16
  temperature: float = 0,
17
17
  time: Optional[float] = DEFAULT_TIME,
18
18
  verbose: bool = False,
19
+ source_file_path: str = None,
20
+ example_file_path: str = None,
21
+ module_name: str = None,
19
22
  ) -> tuple:
20
23
  """
21
24
  Generates a concise example on how to use a given code module properly.
@@ -79,7 +82,10 @@ def context_generator(
79
82
  input_json={
80
83
  "code_module": code_module,
81
84
  "processed_prompt": processed_prompt,
82
- "language": language
85
+ "language": language,
86
+ "source_file_path": source_file_path or "",
87
+ "example_file_path": example_file_path or "",
88
+ "module_name": module_name or ""
83
89
  },
84
90
  strength=strength,
85
91
  temperature=temperature,
@@ -95,6 +101,7 @@ def context_generator(
95
101
  strength=0.5,
96
102
  temperature=temperature,
97
103
  time=time,
104
+ language=language,
98
105
  verbose=verbose
99
106
  )
100
107
  except Exception as e:
@@ -112,6 +119,7 @@ def context_generator(
112
119
  strength=strength,
113
120
  temperature=temperature,
114
121
  time=time,
122
+ language=language,
115
123
  verbose=verbose
116
124
  )
117
125
  total_cost = llm_response['cost'] + unfinished_cost + continue_cost
@@ -149,4 +157,4 @@ if __name__ == "__main__":
149
157
  print("[bold green]Generated Example Code:[/bold green]")
150
158
  print(example_code)
151
159
  print(f"[bold blue]Total Cost: ${total_cost:.6f}[/bold blue]")
152
- print(f"[bold blue]Model Name: {model_name}[/bold blue]")
160
+ print(f"[bold blue]Model Name: {model_name}[/bold blue]")