pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +506 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +537 -0
- pdd/agentic_common.py +533 -770
- pdd/agentic_crash.py +2 -1
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +582 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +27 -9
- pdd/agentic_verify.py +3 -2
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +236 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +113 -48
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +358 -0
- pdd/commands/fix.py +155 -114
- pdd/commands/generate.py +5 -0
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +225 -163
- pdd/commands/sessions.py +284 -0
- pdd/commands/utility.py +12 -7
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +44 -7
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +68 -20
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +208 -6
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +531 -97
- pdd/load_prompt_template.py +15 -34
- pdd/operation_log.py +342 -0
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +122 -97
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +19 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1347 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +217 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +289 -211
- pdd/sync_order.py +304 -0
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +68 -26
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
- pdd_cli-0.0.121.dist-info/RECORD +229 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/sync_determine_operation.py
CHANGED
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
import json
|
|
12
12
|
import hashlib
|
|
13
13
|
import subprocess
|
|
14
|
+
import fnmatch
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from dataclasses import dataclass, field
|
|
16
17
|
from typing import Dict, List, Optional, Any
|
|
@@ -31,10 +32,17 @@ except ImportError:
|
|
|
31
32
|
HAS_MSVCRT = False
|
|
32
33
|
|
|
33
34
|
# Import PDD internal modules
|
|
34
|
-
from pdd.construct_paths import
|
|
35
|
+
from pdd.construct_paths import (
|
|
36
|
+
_detect_context,
|
|
37
|
+
_find_pddrc_file,
|
|
38
|
+
_get_relative_basename,
|
|
39
|
+
_load_pddrc_config,
|
|
40
|
+
construct_paths,
|
|
41
|
+
)
|
|
35
42
|
from pdd.load_prompt_template import load_prompt_template
|
|
36
43
|
from pdd.llm_invoke import llm_invoke
|
|
37
44
|
from pdd.get_language import get_language
|
|
45
|
+
from pdd.template_expander import expand_template
|
|
38
46
|
|
|
39
47
|
# Constants - Use functions for dynamic path resolution
|
|
40
48
|
def get_pdd_dir():
|
|
@@ -55,11 +63,41 @@ META_DIR = get_meta_dir()
|
|
|
55
63
|
LOCKS_DIR = get_locks_dir()
|
|
56
64
|
|
|
57
65
|
# Export constants for other modules
|
|
58
|
-
__all__ = ['PDD_DIR', 'META_DIR', 'LOCKS_DIR', 'Fingerprint', 'RunReport', 'SyncDecision',
|
|
66
|
+
__all__ = ['PDD_DIR', 'META_DIR', 'LOCKS_DIR', 'Fingerprint', 'RunReport', 'SyncDecision',
|
|
59
67
|
'sync_determine_operation', 'analyze_conflict_with_llm', 'read_run_report', 'get_pdd_file_paths',
|
|
60
68
|
'_check_example_success_history']
|
|
61
69
|
|
|
62
70
|
|
|
71
|
+
def _safe_basename(basename: str) -> str:
|
|
72
|
+
"""Sanitize basename for use in metadata filenames.
|
|
73
|
+
|
|
74
|
+
Replaces '/' with '_' to prevent path interpretation when the basename
|
|
75
|
+
contains subdirectory components (e.g., 'core/cloud' -> 'core_cloud').
|
|
76
|
+
"""
|
|
77
|
+
return basename.replace('/', '_')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_name_part(basename: str) -> tuple:
|
|
81
|
+
"""Extract directory and name parts from a subdirectory basename.
|
|
82
|
+
|
|
83
|
+
For subdirectory basenames like 'core/cloud', separates the directory
|
|
84
|
+
prefix from the actual name so that filename patterns can be applied
|
|
85
|
+
correctly.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
basename: The full basename, possibly containing subdirectory path.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (dir_prefix, name_part):
|
|
92
|
+
- 'core/cloud' -> ('core/', 'cloud')
|
|
93
|
+
- 'calculator' -> ('', 'calculator')
|
|
94
|
+
"""
|
|
95
|
+
if '/' in basename:
|
|
96
|
+
dir_part, name_part = basename.rsplit('/', 1)
|
|
97
|
+
return dir_part + '/', name_part
|
|
98
|
+
return '', basename
|
|
99
|
+
|
|
100
|
+
|
|
63
101
|
@dataclass
|
|
64
102
|
class Fingerprint:
|
|
65
103
|
"""Represents the last known good state of a PDD unit."""
|
|
@@ -102,7 +140,7 @@ class SyncLock:
|
|
|
102
140
|
def __init__(self, basename: str, language: str):
|
|
103
141
|
self.basename = basename
|
|
104
142
|
self.language = language
|
|
105
|
-
self.lock_file = get_locks_dir() / f"{basename}_{language}.lock"
|
|
143
|
+
self.lock_file = get_locks_dir() / f"{_safe_basename(basename)}_{language}.lock"
|
|
106
144
|
self.fd = None
|
|
107
145
|
self.current_pid = os.getpid()
|
|
108
146
|
|
|
@@ -212,6 +250,142 @@ def get_extension(language: str) -> str:
|
|
|
212
250
|
return extensions.get(language.lower(), language.lower())
|
|
213
251
|
|
|
214
252
|
|
|
253
|
+
def _resolve_prompts_root(prompts_dir: str) -> Path:
|
|
254
|
+
"""Resolve prompts root relative to the .pddrc location when available."""
|
|
255
|
+
prompts_root = Path(prompts_dir)
|
|
256
|
+
pddrc_path = _find_pddrc_file()
|
|
257
|
+
if pddrc_path and not prompts_root.is_absolute():
|
|
258
|
+
prompts_root = pddrc_path.parent / prompts_root
|
|
259
|
+
|
|
260
|
+
parts = prompts_root.parts
|
|
261
|
+
if "prompts" in parts:
|
|
262
|
+
prompt_index = parts.index("prompts")
|
|
263
|
+
prompts_root = Path(*parts[: prompt_index + 1])
|
|
264
|
+
|
|
265
|
+
return prompts_root
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _relative_basename_for_context(basename: str, context_name: Optional[str]) -> str:
|
|
269
|
+
"""Strip context-specific prefixes from basename when possible."""
|
|
270
|
+
if not context_name:
|
|
271
|
+
return basename
|
|
272
|
+
|
|
273
|
+
pddrc_path = _find_pddrc_file()
|
|
274
|
+
if not pddrc_path:
|
|
275
|
+
return basename
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
config = _load_pddrc_config(pddrc_path)
|
|
279
|
+
except ValueError:
|
|
280
|
+
return basename
|
|
281
|
+
|
|
282
|
+
contexts = config.get("contexts", {})
|
|
283
|
+
context_config = contexts.get(context_name, {})
|
|
284
|
+
defaults = context_config.get("defaults", {})
|
|
285
|
+
|
|
286
|
+
matches = []
|
|
287
|
+
|
|
288
|
+
prompts_dir = defaults.get("prompts_dir", "")
|
|
289
|
+
if prompts_dir:
|
|
290
|
+
normalized = prompts_dir.rstrip("/")
|
|
291
|
+
prefix = normalized
|
|
292
|
+
if normalized == "prompts":
|
|
293
|
+
prefix = ""
|
|
294
|
+
elif normalized.startswith("prompts/"):
|
|
295
|
+
prefix = normalized[len("prompts/"):]
|
|
296
|
+
|
|
297
|
+
if prefix and (basename == prefix or basename.startswith(prefix + "/")):
|
|
298
|
+
relative = basename[len(prefix) + 1 :] if basename != prefix else basename.split("/")[-1]
|
|
299
|
+
matches.append((len(prefix), relative))
|
|
300
|
+
|
|
301
|
+
for pattern in context_config.get("paths", []):
|
|
302
|
+
pattern_base = pattern.rstrip("/**").rstrip("/*")
|
|
303
|
+
if fnmatch.fnmatch(basename, pattern) or \
|
|
304
|
+
basename.startswith(pattern_base + "/") or \
|
|
305
|
+
basename == pattern_base:
|
|
306
|
+
relative = _get_relative_basename(basename, pattern)
|
|
307
|
+
matches.append((len(pattern_base), relative))
|
|
308
|
+
|
|
309
|
+
if not matches:
|
|
310
|
+
return basename
|
|
311
|
+
|
|
312
|
+
matches.sort(key=lambda item: item[0], reverse=True)
|
|
313
|
+
return matches[0][1]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _generate_paths_from_templates(
|
|
317
|
+
basename: str,
|
|
318
|
+
language: str,
|
|
319
|
+
extension: str,
|
|
320
|
+
outputs_config: Dict[str, Any],
|
|
321
|
+
prompt_path: str
|
|
322
|
+
) -> Dict[str, Path]:
|
|
323
|
+
"""
|
|
324
|
+
Generate file paths from template configuration.
|
|
325
|
+
|
|
326
|
+
This function is used by Issue #237 to support extensible output path patterns
|
|
327
|
+
for different project layouts (Next.js, Vue, Python backend, etc.).
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
basename: The relative basename (e.g., 'marketplace/AssetCard' or 'credit_helpers')
|
|
331
|
+
language: The full language name (e.g., 'python', 'typescript')
|
|
332
|
+
extension: The file extension (e.g., 'py', 'tsx')
|
|
333
|
+
outputs_config: The 'outputs' section from .pddrc context config
|
|
334
|
+
prompt_path: The prompt file path to use as fallback
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Dictionary mapping file types ('prompt', 'code', 'test', etc.) to Path objects
|
|
338
|
+
"""
|
|
339
|
+
import logging
|
|
340
|
+
logger = logging.getLogger(__name__)
|
|
341
|
+
|
|
342
|
+
# Extract name parts for template context
|
|
343
|
+
parts = basename.split('/')
|
|
344
|
+
name = parts[-1] if parts else basename
|
|
345
|
+
category = '/'.join(parts[:-1]) if len(parts) > 1 else ''
|
|
346
|
+
|
|
347
|
+
# Build dir_prefix (for legacy template compatibility)
|
|
348
|
+
dir_prefix = '/'.join(parts[:-1]) + '/' if len(parts) > 1 else ''
|
|
349
|
+
|
|
350
|
+
# Build template context
|
|
351
|
+
template_context = {
|
|
352
|
+
'name': name,
|
|
353
|
+
'category': category,
|
|
354
|
+
'dir_prefix': dir_prefix,
|
|
355
|
+
'ext': extension,
|
|
356
|
+
'language': language,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
logger.debug(f"Template context: {template_context}")
|
|
360
|
+
|
|
361
|
+
result = {}
|
|
362
|
+
|
|
363
|
+
# Expand templates for each output type
|
|
364
|
+
for output_type, config in outputs_config.items():
|
|
365
|
+
if isinstance(config, dict) and 'path' in config:
|
|
366
|
+
template = config['path']
|
|
367
|
+
expanded = expand_template(template, template_context)
|
|
368
|
+
result[output_type] = Path(expanded)
|
|
369
|
+
logger.debug(f"Template {output_type}: {template} -> {expanded}")
|
|
370
|
+
|
|
371
|
+
# Ensure prompt is always present (fallback to provided prompt_path)
|
|
372
|
+
if 'prompt' not in result:
|
|
373
|
+
result['prompt'] = Path(prompt_path)
|
|
374
|
+
|
|
375
|
+
# Handle test_files for Bug #156 compatibility
|
|
376
|
+
if 'test' in result:
|
|
377
|
+
test_path = result['test']
|
|
378
|
+
test_dir_path = test_path.parent
|
|
379
|
+
test_stem = f"test_{name}"
|
|
380
|
+
if test_dir_path.exists():
|
|
381
|
+
matching_test_files = sorted(test_dir_path.glob(f"{test_stem}*.{extension}"))
|
|
382
|
+
else:
|
|
383
|
+
matching_test_files = [test_path] if test_path.exists() else []
|
|
384
|
+
result['test_files'] = matching_test_files or [test_path]
|
|
385
|
+
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
|
|
215
389
|
def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts", context_override: Optional[str] = None) -> Dict[str, Path]:
|
|
216
390
|
"""Returns a dictionary mapping file types to their expected Path objects."""
|
|
217
391
|
import logging
|
|
@@ -220,8 +394,27 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
220
394
|
|
|
221
395
|
try:
|
|
222
396
|
# Use construct_paths to get configuration-aware paths
|
|
397
|
+
prompts_root = _resolve_prompts_root(prompts_dir)
|
|
223
398
|
prompt_filename = f"{basename}_{language}.prompt"
|
|
224
|
-
prompt_path = str(
|
|
399
|
+
prompt_path = str(prompts_root / prompt_filename)
|
|
400
|
+
pddrc_path = _find_pddrc_file()
|
|
401
|
+
if pddrc_path:
|
|
402
|
+
try:
|
|
403
|
+
config = _load_pddrc_config(pddrc_path)
|
|
404
|
+
context_name = context_override or _detect_context(Path.cwd(), config, None)
|
|
405
|
+
context_config = config.get('contexts', {}).get(context_name or '', {})
|
|
406
|
+
prompts_dir_config = context_config.get('defaults', {}).get('prompts_dir', '')
|
|
407
|
+
if prompts_dir_config:
|
|
408
|
+
normalized = prompts_dir_config.rstrip('/')
|
|
409
|
+
prefix = normalized
|
|
410
|
+
if normalized == 'prompts':
|
|
411
|
+
prefix = ''
|
|
412
|
+
elif normalized.startswith('prompts/'):
|
|
413
|
+
prefix = normalized[len('prompts/'):]
|
|
414
|
+
if prefix and not (basename == prefix or basename.startswith(prefix + '/')):
|
|
415
|
+
prompt_path = str(prompts_root / prefix / prompt_filename)
|
|
416
|
+
except ValueError:
|
|
417
|
+
pass
|
|
225
418
|
logger.info(f"Checking prompt_path={prompt_path}, exists={Path(prompt_path).exists()}")
|
|
226
419
|
|
|
227
420
|
# Check if prompt file exists - if not, we still need configuration-aware paths
|
|
@@ -237,22 +430,40 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
237
430
|
quiet=True,
|
|
238
431
|
command="sync",
|
|
239
432
|
command_options={"basename": basename, "language": language},
|
|
240
|
-
context_override=context_override
|
|
433
|
+
context_override=context_override,
|
|
434
|
+
path_resolution_mode="cwd"
|
|
241
435
|
)
|
|
242
|
-
|
|
436
|
+
|
|
243
437
|
import logging
|
|
244
438
|
logger = logging.getLogger(__name__)
|
|
245
439
|
logger.info(f"resolved_config: {resolved_config}")
|
|
246
440
|
logger.info(f"output_paths: {output_paths}")
|
|
247
|
-
|
|
441
|
+
|
|
442
|
+
# Issue #237: Check for 'outputs' config for template-based path generation
|
|
443
|
+
outputs_config = resolved_config.get('outputs')
|
|
444
|
+
if outputs_config:
|
|
445
|
+
logger.info(f"Using template-based paths from outputs config")
|
|
446
|
+
context_name = context_override or resolved_config.get('_matched_context')
|
|
447
|
+
basename_for_templates = _relative_basename_for_context(basename, context_name)
|
|
448
|
+
result = _generate_paths_from_templates(
|
|
449
|
+
basename=basename_for_templates,
|
|
450
|
+
language=language,
|
|
451
|
+
extension=extension,
|
|
452
|
+
outputs_config=outputs_config,
|
|
453
|
+
prompt_path=prompt_path
|
|
454
|
+
)
|
|
455
|
+
logger.debug(f"get_pdd_file_paths returning (template-based): {result}")
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
# Legacy path construction (backwards compatibility)
|
|
248
459
|
# Extract directory configuration from resolved_config
|
|
249
460
|
# Note: construct_paths sets tests_dir, examples_dir, code_dir keys
|
|
250
461
|
test_dir = resolved_config.get('tests_dir', 'tests/')
|
|
251
462
|
example_dir = resolved_config.get('examples_dir', 'examples/')
|
|
252
463
|
code_dir = resolved_config.get('code_dir', './')
|
|
253
|
-
|
|
464
|
+
|
|
254
465
|
logger.info(f"Extracted dirs - test: {test_dir}, example: {example_dir}, code: {code_dir}")
|
|
255
|
-
|
|
466
|
+
|
|
256
467
|
# Ensure directories end with /
|
|
257
468
|
if test_dir and not test_dir.endswith('/'):
|
|
258
469
|
test_dir = test_dir + '/'
|
|
@@ -260,14 +471,45 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
260
471
|
example_dir = example_dir + '/'
|
|
261
472
|
if code_dir and not code_dir.endswith('/'):
|
|
262
473
|
code_dir = code_dir + '/'
|
|
263
|
-
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
474
|
+
|
|
475
|
+
# Extract directory and name parts for subdirectory basename support
|
|
476
|
+
dir_prefix, name_part = _extract_name_part(basename)
|
|
477
|
+
|
|
478
|
+
# Get explicit config paths (these are the SOURCE OF TRUTH when configured)
|
|
479
|
+
# These should be used directly, NOT combined with dir_prefix
|
|
480
|
+
generate_output_path = resolved_config.get('generate_output_path', '')
|
|
481
|
+
example_output_path = resolved_config.get('example_output_path', '')
|
|
482
|
+
test_output_path = resolved_config.get('test_output_path', '')
|
|
483
|
+
|
|
484
|
+
# Construct paths: use explicit config paths directly when configured,
|
|
485
|
+
# otherwise fall back to old behavior with dir_prefix for backwards compat
|
|
486
|
+
|
|
487
|
+
# Code path
|
|
488
|
+
if generate_output_path and generate_output_path.endswith('/'):
|
|
489
|
+
# Explicit complete directory - use directly with just filename
|
|
490
|
+
code_path = f"{generate_output_path}{name_part}.{extension}"
|
|
491
|
+
else:
|
|
492
|
+
# Old behavior - use code_dir + dir_prefix
|
|
493
|
+
code_path = f"{code_dir}{dir_prefix}{name_part}.{extension}"
|
|
494
|
+
|
|
495
|
+
# Example path
|
|
496
|
+
if example_output_path and example_output_path.endswith('/'):
|
|
497
|
+
# Explicit complete directory - use directly with just filename
|
|
498
|
+
example_path = f"{example_output_path}{name_part}_example.{extension}"
|
|
499
|
+
else:
|
|
500
|
+
# Old behavior - use example_dir + dir_prefix
|
|
501
|
+
example_path = f"{example_dir}{dir_prefix}{name_part}_example.{extension}"
|
|
502
|
+
|
|
503
|
+
# Test path
|
|
504
|
+
if test_output_path and test_output_path.endswith('/'):
|
|
505
|
+
# Explicit complete directory - use directly with just filename
|
|
506
|
+
test_path = f"{test_output_path}test_{name_part}.{extension}"
|
|
507
|
+
else:
|
|
508
|
+
# Old behavior - use test_dir + dir_prefix
|
|
509
|
+
test_path = f"{test_dir}{dir_prefix}test_{name_part}.{extension}"
|
|
510
|
+
|
|
269
511
|
logger.debug(f"Final paths: test={test_path}, example={example_path}, code={code_path}")
|
|
270
|
-
|
|
512
|
+
|
|
271
513
|
# Convert to Path objects
|
|
272
514
|
test_path = Path(test_path)
|
|
273
515
|
example_path = Path(example_path)
|
|
@@ -275,7 +517,7 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
275
517
|
|
|
276
518
|
# Bug #156: Find all matching test files
|
|
277
519
|
test_dir_path = test_path.parent
|
|
278
|
-
test_stem = f"test_{
|
|
520
|
+
test_stem = f"test_{name_part}"
|
|
279
521
|
if test_dir_path.exists():
|
|
280
522
|
matching_test_files = sorted(test_dir_path.glob(f"{test_stem}*.{extension}"))
|
|
281
523
|
else:
|
|
@@ -296,16 +538,17 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
296
538
|
import logging
|
|
297
539
|
logger = logging.getLogger(__name__)
|
|
298
540
|
logger.debug(f"construct_paths failed for non-existent prompt, using defaults: {e}")
|
|
299
|
-
|
|
541
|
+
dir_prefix, name_part = _extract_name_part(basename)
|
|
542
|
+
fallback_test_path = Path(f"{dir_prefix}test_{name_part}.{extension}")
|
|
300
543
|
# Bug #156: Find matching test files even in fallback
|
|
301
544
|
if Path('.').exists():
|
|
302
|
-
fallback_matching = sorted(Path('.').glob(f"test_{
|
|
545
|
+
fallback_matching = sorted(Path('.').glob(f"{dir_prefix}test_{name_part}*.{extension}"))
|
|
303
546
|
else:
|
|
304
547
|
fallback_matching = [fallback_test_path] if fallback_test_path.exists() else []
|
|
305
548
|
return {
|
|
306
549
|
'prompt': Path(prompt_path),
|
|
307
|
-
'code': Path(f"{
|
|
308
|
-
'example': Path(f"{
|
|
550
|
+
'code': Path(f"{dir_prefix}{name_part}.{extension}"),
|
|
551
|
+
'example': Path(f"{dir_prefix}{name_part}_example.{extension}"),
|
|
309
552
|
'test': fallback_test_path,
|
|
310
553
|
'test_files': fallback_matching or [fallback_test_path] # Bug #156
|
|
311
554
|
}
|
|
@@ -321,9 +564,28 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
321
564
|
quiet=True,
|
|
322
565
|
command="sync", # Use sync command to get more tolerant path handling
|
|
323
566
|
command_options={"basename": basename, "language": language},
|
|
324
|
-
context_override=context_override
|
|
567
|
+
context_override=context_override,
|
|
568
|
+
path_resolution_mode="cwd"
|
|
325
569
|
)
|
|
326
|
-
|
|
570
|
+
|
|
571
|
+
# Issue #237: Check for 'outputs' config for template-based path generation
|
|
572
|
+
# This must be checked even when prompt EXISTS (not just when it doesn't exist)
|
|
573
|
+
outputs_config = resolved_config.get('outputs')
|
|
574
|
+
if outputs_config:
|
|
575
|
+
extension = get_extension(language)
|
|
576
|
+
logger.info(f"Using template-based paths from outputs config (prompt exists)")
|
|
577
|
+
context_name = context_override or resolved_config.get('_matched_context')
|
|
578
|
+
basename_for_templates = _relative_basename_for_context(basename, context_name)
|
|
579
|
+
result = _generate_paths_from_templates(
|
|
580
|
+
basename=basename_for_templates,
|
|
581
|
+
language=language,
|
|
582
|
+
extension=extension,
|
|
583
|
+
outputs_config=outputs_config,
|
|
584
|
+
prompt_path=prompt_path
|
|
585
|
+
)
|
|
586
|
+
logger.debug(f"get_pdd_file_paths returning (template-based, prompt exists): {result}")
|
|
587
|
+
return result
|
|
588
|
+
|
|
327
589
|
# For sync command, output_file_paths contains the configured paths
|
|
328
590
|
# Extract the code path from output_file_paths
|
|
329
591
|
code_path = output_file_paths.get('generate_output_path', '')
|
|
@@ -333,10 +595,18 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
333
595
|
if not code_path:
|
|
334
596
|
# Fallback to constructing from basename with configuration
|
|
335
597
|
extension = get_extension(language)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
598
|
+
generate_output_path = resolved_config.get('generate_output_path', '')
|
|
599
|
+
dir_prefix, name_part = _extract_name_part(basename)
|
|
600
|
+
|
|
601
|
+
# Use explicit config path directly when configured (ending with /)
|
|
602
|
+
if generate_output_path and generate_output_path.endswith('/'):
|
|
603
|
+
code_path = f"{generate_output_path}{name_part}.{extension}"
|
|
604
|
+
else:
|
|
605
|
+
# Old behavior - use path + dir_prefix
|
|
606
|
+
code_dir = generate_output_path or './'
|
|
607
|
+
if not code_dir.endswith('/'):
|
|
608
|
+
code_dir = code_dir + '/'
|
|
609
|
+
code_path = f"{code_dir}{dir_prefix}{name_part}.{extension}"
|
|
340
610
|
|
|
341
611
|
# Get configured paths for example and test files using construct_paths
|
|
342
612
|
# Note: construct_paths requires files to exist, so we need to handle the case
|
|
@@ -353,26 +623,31 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
353
623
|
try:
|
|
354
624
|
# Get example path using example command
|
|
355
625
|
# Pass path_resolution_mode="cwd" so paths resolve relative to CWD (not project root)
|
|
626
|
+
# Pass basename in command_options to preserve subdirectory structure
|
|
356
627
|
_, _, example_output_paths, _ = construct_paths(
|
|
357
628
|
input_file_paths={"prompt_file": prompt_path, "code_file": code_path},
|
|
358
|
-
force=True, quiet=True, command="example",
|
|
629
|
+
force=True, quiet=True, command="example",
|
|
630
|
+
command_options={"basename": basename},
|
|
359
631
|
context_override=context_override,
|
|
360
632
|
path_resolution_mode="cwd"
|
|
361
633
|
)
|
|
362
|
-
|
|
634
|
+
dir_prefix, name_part = _extract_name_part(basename)
|
|
635
|
+
example_path = Path(example_output_paths.get('output', f"{dir_prefix}{name_part}_example.{get_extension(language)}"))
|
|
363
636
|
|
|
364
637
|
# Get test path using test command - handle case where test file doesn't exist yet
|
|
638
|
+
# Pass basename in command_options to preserve subdirectory structure
|
|
365
639
|
try:
|
|
366
640
|
_, _, test_output_paths, _ = construct_paths(
|
|
367
641
|
input_file_paths={"prompt_file": prompt_path, "code_file": code_path},
|
|
368
|
-
force=True, quiet=True, command="test",
|
|
642
|
+
force=True, quiet=True, command="test",
|
|
643
|
+
command_options={"basename": basename},
|
|
369
644
|
context_override=context_override,
|
|
370
645
|
path_resolution_mode="cwd"
|
|
371
646
|
)
|
|
372
|
-
test_path = Path(test_output_paths.get('output', f"test_{
|
|
647
|
+
test_path = Path(test_output_paths.get('output', f"{dir_prefix}test_{name_part}.{get_extension(language)}"))
|
|
373
648
|
except FileNotFoundError:
|
|
374
649
|
# Test file doesn't exist yet - create default path
|
|
375
|
-
test_path = Path(f"test_{
|
|
650
|
+
test_path = Path(f"{dir_prefix}test_{name_part}.{get_extension(language)}")
|
|
376
651
|
|
|
377
652
|
finally:
|
|
378
653
|
# Clean up temporary file if we created it
|
|
@@ -391,25 +666,29 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
391
666
|
try:
|
|
392
667
|
# Get configured directories by using construct_paths with just the prompt file
|
|
393
668
|
# Pass path_resolution_mode="cwd" so paths resolve relative to CWD (not project root)
|
|
669
|
+
# Pass basename in command_options to preserve subdirectory structure
|
|
394
670
|
_, _, example_output_paths, _ = construct_paths(
|
|
395
671
|
input_file_paths={"prompt_file": prompt_path},
|
|
396
|
-
force=True, quiet=True, command="example",
|
|
672
|
+
force=True, quiet=True, command="example",
|
|
673
|
+
command_options={"basename": basename},
|
|
397
674
|
context_override=context_override,
|
|
398
675
|
path_resolution_mode="cwd"
|
|
399
676
|
)
|
|
400
|
-
|
|
677
|
+
dir_prefix, name_part = _extract_name_part(basename)
|
|
678
|
+
example_path = Path(example_output_paths.get('output', f"{dir_prefix}{name_part}_example.{get_extension(language)}"))
|
|
401
679
|
|
|
402
680
|
try:
|
|
403
681
|
_, _, test_output_paths, _ = construct_paths(
|
|
404
682
|
input_file_paths={"prompt_file": prompt_path},
|
|
405
|
-
force=True, quiet=True, command="test",
|
|
683
|
+
force=True, quiet=True, command="test",
|
|
684
|
+
command_options={"basename": basename},
|
|
406
685
|
context_override=context_override,
|
|
407
686
|
path_resolution_mode="cwd"
|
|
408
687
|
)
|
|
409
|
-
test_path = Path(test_output_paths.get('output', f"test_{
|
|
688
|
+
test_path = Path(test_output_paths.get('output', f"{dir_prefix}test_{name_part}.{get_extension(language)}"))
|
|
410
689
|
except Exception:
|
|
411
690
|
# If test path construction fails, use default naming
|
|
412
|
-
test_path = Path(f"test_{
|
|
691
|
+
test_path = Path(f"{dir_prefix}test_{name_part}.{get_extension(language)}")
|
|
413
692
|
|
|
414
693
|
except Exception:
|
|
415
694
|
# Final fallback to deriving from code path if all else fails
|
|
@@ -429,7 +708,8 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
429
708
|
|
|
430
709
|
# Bug #156: Find all matching test files
|
|
431
710
|
test_dir = test_path.parent
|
|
432
|
-
|
|
711
|
+
_, name_part_for_glob = _extract_name_part(basename)
|
|
712
|
+
test_stem = f"test_{name_part_for_glob}"
|
|
433
713
|
extension = get_extension(language)
|
|
434
714
|
if test_dir.exists():
|
|
435
715
|
matching_test_files = sorted(test_dir.glob(f"{test_stem}*.{extension}"))
|
|
@@ -443,22 +723,24 @@ def get_pdd_file_paths(basename: str, language: str, prompts_dir: str = "prompts
|
|
|
443
723
|
'test': test_path,
|
|
444
724
|
'test_files': matching_test_files or [test_path] # Bug #156: All matching test files
|
|
445
725
|
}
|
|
446
|
-
|
|
726
|
+
|
|
447
727
|
except Exception as e:
|
|
448
728
|
# Fallback to simple naming if construct_paths fails
|
|
449
729
|
extension = get_extension(language)
|
|
450
|
-
|
|
730
|
+
dir_prefix, name_part = _extract_name_part(basename)
|
|
731
|
+
test_path = Path(f"{dir_prefix}test_{name_part}.{extension}")
|
|
451
732
|
# Bug #156: Try to find matching test files even in fallback
|
|
452
733
|
test_dir = Path('.')
|
|
453
|
-
test_stem = f"test_{
|
|
734
|
+
test_stem = f"{dir_prefix}test_{name_part}"
|
|
454
735
|
if test_dir.exists():
|
|
455
736
|
matching_test_files = sorted(test_dir.glob(f"{test_stem}*.{extension}"))
|
|
456
737
|
else:
|
|
457
738
|
matching_test_files = [test_path] if test_path.exists() else []
|
|
739
|
+
prompts_root = _resolve_prompts_root(prompts_dir)
|
|
458
740
|
return {
|
|
459
|
-
'prompt':
|
|
460
|
-
'code': Path(f"{
|
|
461
|
-
'example': Path(f"{
|
|
741
|
+
'prompt': prompts_root / f"{basename}_{language}.prompt",
|
|
742
|
+
'code': Path(f"{dir_prefix}{name_part}.{extension}"),
|
|
743
|
+
'example': Path(f"{dir_prefix}{name_part}_example.{extension}"),
|
|
462
744
|
'test': test_path,
|
|
463
745
|
'test_files': matching_test_files or [test_path] # Bug #156: All matching test files
|
|
464
746
|
}
|
|
@@ -483,7 +765,7 @@ def read_fingerprint(basename: str, language: str) -> Optional[Fingerprint]:
|
|
|
483
765
|
"""Reads and validates the JSON fingerprint file."""
|
|
484
766
|
meta_dir = get_meta_dir()
|
|
485
767
|
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
486
|
-
fingerprint_file = meta_dir / f"{basename}_{language}.json"
|
|
768
|
+
fingerprint_file = meta_dir / f"{_safe_basename(basename)}_{language}.json"
|
|
487
769
|
|
|
488
770
|
if not fingerprint_file.exists():
|
|
489
771
|
return None
|
|
@@ -510,7 +792,7 @@ def read_run_report(basename: str, language: str) -> Optional[RunReport]:
|
|
|
510
792
|
"""Reads and validates the JSON run report file."""
|
|
511
793
|
meta_dir = get_meta_dir()
|
|
512
794
|
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
513
|
-
run_report_file = meta_dir / f"{basename}_{language}_run.json"
|
|
795
|
+
run_report_file = meta_dir / f"{_safe_basename(basename)}_{language}_run.json"
|
|
514
796
|
|
|
515
797
|
if not run_report_file.exists():
|
|
516
798
|
return None
|
|
@@ -888,7 +1170,7 @@ def _check_example_success_history(basename: str, language: str) -> bool:
|
|
|
888
1170
|
|
|
889
1171
|
# Strategy 2b: Look for historical run reports with exit_code == 0
|
|
890
1172
|
# Check all run report files in the meta directory that match the pattern
|
|
891
|
-
run_report_pattern = f"{basename}_{language}_run"
|
|
1173
|
+
run_report_pattern = f"{_safe_basename(basename)}_{language}_run"
|
|
892
1174
|
for file in meta_dir.glob(f"{run_report_pattern}*.json"):
|
|
893
1175
|
try:
|
|
894
1176
|
with open(file, 'r') as f:
|