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/construct_paths.py
CHANGED
|
@@ -73,6 +73,146 @@ def list_available_contexts(start_path: Optional[Path] = None) -> list[str]:
|
|
|
73
73
|
names = sorted(contexts.keys()) if isinstance(contexts, dict) else []
|
|
74
74
|
return names or ["default"]
|
|
75
75
|
|
|
76
|
+
def _match_path_to_contexts(
|
|
77
|
+
path_str: str,
|
|
78
|
+
contexts: Dict[str, Any],
|
|
79
|
+
use_specificity: bool = False,
|
|
80
|
+
is_absolute: bool = False
|
|
81
|
+
) -> Optional[str]:
|
|
82
|
+
"""
|
|
83
|
+
Core pattern matching logic - matches a path against context patterns.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
path_str: Path to match (can be relative or absolute)
|
|
87
|
+
contexts: The contexts dict from .pddrc config
|
|
88
|
+
use_specificity: If True, return most specific match; else first match
|
|
89
|
+
is_absolute: If True, use absolute path matching with "*/" prefix
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Context name or None
|
|
93
|
+
"""
|
|
94
|
+
matches = []
|
|
95
|
+
for context_name, context_config in contexts.items():
|
|
96
|
+
if context_name == 'default':
|
|
97
|
+
continue
|
|
98
|
+
for path_pattern in context_config.get('paths', []):
|
|
99
|
+
pattern_base = path_pattern.rstrip('/**').rstrip('/*')
|
|
100
|
+
|
|
101
|
+
# Check for match - handle both absolute and relative paths
|
|
102
|
+
matched = False
|
|
103
|
+
if is_absolute:
|
|
104
|
+
# For absolute paths (CWD-based detection), use existing logic
|
|
105
|
+
if fnmatch.fnmatch(path_str, f"*/{path_pattern}") or \
|
|
106
|
+
fnmatch.fnmatch(path_str, path_pattern) or \
|
|
107
|
+
path_str.endswith(f"/{pattern_base}"):
|
|
108
|
+
matched = True
|
|
109
|
+
else:
|
|
110
|
+
# For relative paths (file-based detection)
|
|
111
|
+
if fnmatch.fnmatch(path_str, path_pattern) or \
|
|
112
|
+
path_str.startswith(pattern_base + '/') or \
|
|
113
|
+
path_str.startswith(pattern_base):
|
|
114
|
+
matched = True
|
|
115
|
+
|
|
116
|
+
if matched:
|
|
117
|
+
if not use_specificity:
|
|
118
|
+
return context_name # First match wins
|
|
119
|
+
matches.append((context_name, len(pattern_base)))
|
|
120
|
+
|
|
121
|
+
if matches:
|
|
122
|
+
matches.sort(key=lambda x: x[1], reverse=True)
|
|
123
|
+
return matches[0][0]
|
|
124
|
+
|
|
125
|
+
return 'default' if 'default' in contexts else None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _detect_context_from_basename(basename: str, config: Dict[str, Any]) -> Optional[str]:
|
|
129
|
+
"""Detect context by matching a sync basename against prompts_dir prefixes or paths patterns."""
|
|
130
|
+
if not basename:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
contexts = config.get('contexts', {})
|
|
134
|
+
matches = []
|
|
135
|
+
|
|
136
|
+
for context_name, context_config in contexts.items():
|
|
137
|
+
if context_name == 'default':
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
defaults = context_config.get('defaults', {})
|
|
141
|
+
prompts_dir = defaults.get('prompts_dir', '')
|
|
142
|
+
if prompts_dir:
|
|
143
|
+
normalized = prompts_dir.rstrip('/')
|
|
144
|
+
prefix = normalized
|
|
145
|
+
if normalized == 'prompts':
|
|
146
|
+
prefix = ''
|
|
147
|
+
elif normalized.startswith('prompts/'):
|
|
148
|
+
prefix = normalized[len('prompts/'):]
|
|
149
|
+
|
|
150
|
+
if prefix and (basename == prefix or basename.startswith(prefix + '/')):
|
|
151
|
+
matches.append((context_name, len(prefix)))
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
for path_pattern in context_config.get('paths', []):
|
|
155
|
+
pattern_base = path_pattern.rstrip('/**').rstrip('/*')
|
|
156
|
+
if fnmatch.fnmatch(basename, path_pattern) or \
|
|
157
|
+
basename.startswith(pattern_base + '/') or \
|
|
158
|
+
basename == pattern_base:
|
|
159
|
+
matches.append((context_name, len(pattern_base)))
|
|
160
|
+
|
|
161
|
+
if not matches:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
matches.sort(key=lambda item: item[1], reverse=True)
|
|
165
|
+
return matches[0][0]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _get_relative_basename(input_path: str, pattern: str) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Compute basename relative to the matched pattern base.
|
|
171
|
+
|
|
172
|
+
This is critical for Issue #237: when a context pattern like
|
|
173
|
+
'frontend/components/**' matches 'frontend/components/marketplace/AssetCard',
|
|
174
|
+
we need to return 'marketplace/AssetCard' (relative to pattern base),
|
|
175
|
+
not the full path which would cause double-pathing in output.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
input_path: The full input path (e.g., 'frontend/components/marketplace/AssetCard')
|
|
179
|
+
pattern: The matching pattern (e.g., 'frontend/components/**')
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Path relative to the pattern base (e.g., 'marketplace/AssetCard')
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
>>> _get_relative_basename('frontend/components/marketplace/AssetCard', 'frontend/components/**')
|
|
186
|
+
'marketplace/AssetCard'
|
|
187
|
+
>>> _get_relative_basename('backend/utils/credit_helpers', 'backend/utils/**')
|
|
188
|
+
'credit_helpers'
|
|
189
|
+
>>> _get_relative_basename('unknown/path', 'other/**')
|
|
190
|
+
'unknown/path' # No match, return as-is
|
|
191
|
+
"""
|
|
192
|
+
# Strip glob patterns to get the base directory
|
|
193
|
+
pattern_base = pattern.rstrip('/**').rstrip('/*').rstrip('*')
|
|
194
|
+
|
|
195
|
+
# Remove trailing slash from pattern base if present
|
|
196
|
+
pattern_base = pattern_base.rstrip('/')
|
|
197
|
+
|
|
198
|
+
# Check if input path starts with pattern base
|
|
199
|
+
if input_path.startswith(pattern_base + '/'):
|
|
200
|
+
# Return the portion after pattern_base/
|
|
201
|
+
return input_path[len(pattern_base) + 1:]
|
|
202
|
+
elif input_path.startswith(pattern_base) and len(input_path) > len(pattern_base):
|
|
203
|
+
# Handle case where pattern_base has no trailing content
|
|
204
|
+
remainder = input_path[len(pattern_base):]
|
|
205
|
+
if remainder.startswith('/'):
|
|
206
|
+
return remainder[1:]
|
|
207
|
+
return remainder
|
|
208
|
+
elif input_path == pattern_base:
|
|
209
|
+
# Exact match - return just the last component
|
|
210
|
+
return input_path.split('/')[-1] if '/' in input_path else input_path
|
|
211
|
+
|
|
212
|
+
# No match - return as-is (fallback for default context)
|
|
213
|
+
return input_path
|
|
214
|
+
|
|
215
|
+
|
|
76
216
|
def _detect_context(current_dir: Path, config: Dict[str, Any], context_override: Optional[str] = None) -> Optional[str]:
|
|
77
217
|
"""Detect the appropriate context based on current directory path."""
|
|
78
218
|
if context_override:
|
|
@@ -82,28 +222,82 @@ def _detect_context(current_dir: Path, config: Dict[str, Any], context_override:
|
|
|
82
222
|
available = list(contexts.keys())
|
|
83
223
|
raise ValueError(f"Unknown context '{context_override}'. Available contexts: {available}")
|
|
84
224
|
return context_override
|
|
85
|
-
|
|
225
|
+
|
|
86
226
|
contexts = config.get('contexts', {})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
227
|
+
return _match_path_to_contexts(str(current_dir), contexts, use_specificity=False, is_absolute=True)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def detect_context_for_file(file_path: str, repo_root: Optional[str] = None) -> Tuple[Optional[str], Dict[str, Any]]:
|
|
231
|
+
"""
|
|
232
|
+
Detect the appropriate context for a file path based on .pddrc configuration.
|
|
233
|
+
|
|
234
|
+
This function finds the most specific matching context by comparing pattern lengths.
|
|
235
|
+
For example, 'backend/functions/utils/**' is more specific than 'backend/**'.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
file_path: Path to the file (can be absolute or relative)
|
|
239
|
+
repo_root: Optional repository root path. If not provided, will be detected.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Tuple of (context_name, context_config_defaults) or (None, {}) if no match.
|
|
243
|
+
"""
|
|
244
|
+
# Find repo root if not provided
|
|
245
|
+
if repo_root is None:
|
|
246
|
+
pddrc_path = _find_pddrc_file(Path(file_path).parent)
|
|
247
|
+
if pddrc_path:
|
|
248
|
+
repo_root = str(pddrc_path.parent)
|
|
249
|
+
else:
|
|
250
|
+
try:
|
|
251
|
+
import git
|
|
252
|
+
repo = git.Repo(file_path, search_parent_directories=True)
|
|
253
|
+
repo_root = repo.working_tree_dir
|
|
254
|
+
except:
|
|
255
|
+
repo_root = os.getcwd()
|
|
256
|
+
|
|
257
|
+
# Make file_path relative to repo_root for matching
|
|
258
|
+
file_path_abs = os.path.abspath(file_path)
|
|
259
|
+
repo_root_abs = os.path.abspath(repo_root)
|
|
260
|
+
|
|
261
|
+
if file_path_abs.startswith(repo_root_abs):
|
|
262
|
+
relative_path = os.path.relpath(file_path_abs, repo_root_abs)
|
|
263
|
+
else:
|
|
264
|
+
relative_path = file_path
|
|
265
|
+
|
|
266
|
+
# Find and load .pddrc
|
|
267
|
+
pddrc_path = _find_pddrc_file(Path(repo_root))
|
|
268
|
+
if not pddrc_path:
|
|
269
|
+
return None, {}
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
config = _load_pddrc_config(pddrc_path)
|
|
273
|
+
except ValueError:
|
|
274
|
+
return None, {}
|
|
275
|
+
|
|
276
|
+
contexts = config.get('contexts', {})
|
|
277
|
+
|
|
278
|
+
# First, try to match against prompts_dir for each context
|
|
279
|
+
# This allows prompt files to be detected even when paths pattern only matches code files
|
|
280
|
+
prompts_dir_matches = []
|
|
90
281
|
for context_name, context_config in contexts.items():
|
|
91
282
|
if context_name == 'default':
|
|
92
|
-
continue
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
283
|
+
continue
|
|
284
|
+
prompts_dir = context_config.get('defaults', {}).get('prompts_dir', '')
|
|
285
|
+
if prompts_dir:
|
|
286
|
+
prompts_dir_normalized = prompts_dir.rstrip('/')
|
|
287
|
+
if relative_path.startswith(prompts_dir_normalized + '/') or relative_path == prompts_dir_normalized:
|
|
288
|
+
# Track match with specificity (length of prompts_dir)
|
|
289
|
+
prompts_dir_matches.append((context_name, len(prompts_dir_normalized)))
|
|
290
|
+
|
|
291
|
+
# Return most specific prompts_dir match if any
|
|
292
|
+
if prompts_dir_matches:
|
|
293
|
+
prompts_dir_matches.sort(key=lambda x: x[1], reverse=True)
|
|
294
|
+
matched_context = prompts_dir_matches[0][0]
|
|
295
|
+
return matched_context, _get_context_config(config, matched_context)
|
|
296
|
+
|
|
297
|
+
# Fall back to existing paths pattern matching
|
|
298
|
+
context_name = _match_path_to_contexts(relative_path, contexts, use_specificity=True, is_absolute=False)
|
|
299
|
+
return context_name, _get_context_config(config, context_name)
|
|
300
|
+
|
|
107
301
|
|
|
108
302
|
def _get_context_config(config: Dict[str, Any], context_name: Optional[str]) -> Dict[str, Any]:
|
|
109
303
|
"""Get configuration settings for the specified context."""
|
|
@@ -121,7 +315,7 @@ def _resolve_config_hierarchy(
|
|
|
121
315
|
) -> Dict[str, Any]:
|
|
122
316
|
"""Apply configuration hierarchy: CLI > context > environment > defaults."""
|
|
123
317
|
resolved = {}
|
|
124
|
-
|
|
318
|
+
|
|
125
319
|
# Configuration keys to resolve
|
|
126
320
|
config_keys = {
|
|
127
321
|
'generate_output_path': 'PDD_GENERATE_OUTPUT_PATH',
|
|
@@ -135,7 +329,7 @@ def _resolve_config_hierarchy(
|
|
|
135
329
|
'budget': None,
|
|
136
330
|
'max_attempts': None,
|
|
137
331
|
}
|
|
138
|
-
|
|
332
|
+
|
|
139
333
|
for config_key, env_var in config_keys.items():
|
|
140
334
|
# 1. CLI options (highest priority)
|
|
141
335
|
if config_key in cli_options and cli_options[config_key] is not None:
|
|
@@ -147,10 +341,56 @@ def _resolve_config_hierarchy(
|
|
|
147
341
|
elif env_var and env_var in env_vars:
|
|
148
342
|
resolved[config_key] = env_vars[env_var]
|
|
149
343
|
# 4. Defaults are handled elsewhere
|
|
150
|
-
|
|
344
|
+
|
|
345
|
+
# Issue #237: Pass through 'outputs' config for template-based path generation
|
|
346
|
+
# This enables extensible project layouts (Next.js, Vue, Python, Go, etc.)
|
|
347
|
+
if 'outputs' in context_config:
|
|
348
|
+
resolved['outputs'] = context_config['outputs']
|
|
349
|
+
|
|
151
350
|
return resolved
|
|
152
351
|
|
|
153
352
|
|
|
353
|
+
def get_tests_dir_from_config(start_path: Optional[Path] = None) -> Optional[Path]:
|
|
354
|
+
"""
|
|
355
|
+
Get the tests directory from .pddrc configuration.
|
|
356
|
+
|
|
357
|
+
Searches for .pddrc, detects the appropriate context, and returns the
|
|
358
|
+
configured test_output_path as a Path object.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
start_path: Starting directory for .pddrc search. Defaults to CWD.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Path to tests directory if configured, None otherwise.
|
|
365
|
+
"""
|
|
366
|
+
if start_path is None:
|
|
367
|
+
start_path = Path.cwd()
|
|
368
|
+
|
|
369
|
+
# Find and load .pddrc
|
|
370
|
+
pddrc_path = _find_pddrc_file(start_path)
|
|
371
|
+
if not pddrc_path:
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
config = _load_pddrc_config(pddrc_path)
|
|
376
|
+
except ValueError:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
# Detect context and get its config
|
|
380
|
+
context_name = _detect_context(start_path, config)
|
|
381
|
+
context_config = _get_context_config(config, context_name)
|
|
382
|
+
|
|
383
|
+
# Check context config first, then env var
|
|
384
|
+
test_output_path = context_config.get('test_output_path')
|
|
385
|
+
if not test_output_path:
|
|
386
|
+
test_output_path = os.environ.get('PDD_TEST_OUTPUT_PATH')
|
|
387
|
+
|
|
388
|
+
if test_output_path:
|
|
389
|
+
return Path(test_output_path)
|
|
390
|
+
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
154
394
|
def _read_file(path: Path) -> str:
|
|
155
395
|
"""Read a text file safely and return its contents."""
|
|
156
396
|
try:
|
|
@@ -463,9 +703,30 @@ def construct_paths(
|
|
|
463
703
|
pddrc_config = _load_pddrc_config(pddrc_path)
|
|
464
704
|
|
|
465
705
|
# Detect appropriate context
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
706
|
+
# Priority: context_override > file-based detection > CWD-based detection
|
|
707
|
+
if context_override:
|
|
708
|
+
# Delegate validation to _detect_context to avoid duplicate validation logic
|
|
709
|
+
context = _detect_context(Path.cwd(), pddrc_config, context_override)
|
|
710
|
+
else:
|
|
711
|
+
# Try file-based detection when prompt file is provided
|
|
712
|
+
prompt_file_str = input_file_paths.get('prompt_file') if input_file_paths else None
|
|
713
|
+
if prompt_file_str and Path(prompt_file_str).exists():
|
|
714
|
+
detected_context, _ = detect_context_for_file(prompt_file_str)
|
|
715
|
+
if detected_context:
|
|
716
|
+
context = detected_context
|
|
717
|
+
else:
|
|
718
|
+
context = _detect_context(Path.cwd(), pddrc_config, None)
|
|
719
|
+
else:
|
|
720
|
+
basename_hint = command_options.get("basename")
|
|
721
|
+
if basename_hint:
|
|
722
|
+
detected_context = _detect_context_from_basename(basename_hint, pddrc_config)
|
|
723
|
+
if detected_context:
|
|
724
|
+
context = detected_context
|
|
725
|
+
else:
|
|
726
|
+
context = _detect_context(Path.cwd(), pddrc_config, None)
|
|
727
|
+
else:
|
|
728
|
+
context = _detect_context(Path.cwd(), pddrc_config, None)
|
|
729
|
+
|
|
469
730
|
# Get context-specific configuration
|
|
470
731
|
context_config = _get_context_config(pddrc_config, context)
|
|
471
732
|
original_context_config = context_config.copy() # Store original before modifications
|
|
@@ -476,9 +737,15 @@ def construct_paths(
|
|
|
476
737
|
# Apply configuration hierarchy
|
|
477
738
|
env_vars = dict(os.environ)
|
|
478
739
|
resolved_config = _resolve_config_hierarchy(command_options, context_config, env_vars)
|
|
479
|
-
|
|
740
|
+
|
|
741
|
+
# Issue #237: Track matched context for debugging
|
|
742
|
+
resolved_config['_matched_context'] = context or 'default'
|
|
743
|
+
|
|
480
744
|
# Update command_options with resolved configuration for internal use
|
|
745
|
+
# Exclude internal metadata keys (prefixed with _) from command_options
|
|
481
746
|
for key, value in resolved_config.items():
|
|
747
|
+
if key.startswith('_'):
|
|
748
|
+
continue # Skip internal metadata like _matched_context
|
|
482
749
|
if key not in command_options or command_options[key] is None:
|
|
483
750
|
command_options[key] = value
|
|
484
751
|
|
|
@@ -531,7 +798,10 @@ def construct_paths(
|
|
|
531
798
|
else:
|
|
532
799
|
# Fall back to context-aware logic
|
|
533
800
|
# Use original_context_config to avoid checking augmented config with env vars
|
|
534
|
-
if original_context_config and
|
|
801
|
+
if original_context_config and (
|
|
802
|
+
'prompts_dir' in original_context_config or
|
|
803
|
+
any(key.endswith('_output_path') for key in original_context_config)
|
|
804
|
+
):
|
|
535
805
|
# For configured contexts, use prompts_dir from config if provided,
|
|
536
806
|
# otherwise default to "prompts" at the same level as output dirs
|
|
537
807
|
resolved_config["prompts_dir"] = original_context_config.get("prompts_dir", "prompts")
|
|
@@ -543,9 +813,22 @@ def construct_paths(
|
|
|
543
813
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
544
814
|
|
|
545
815
|
resolved_config["tests_dir"] = str(Path(output_paths_str.get("test_output_path", "tests")).parent)
|
|
546
|
-
|
|
816
|
+
|
|
817
|
+
# Determine examples_dir for auto-deps scanning
|
|
818
|
+
# NOTE: outputs.example.path is for OUTPUT only (where to write examples),
|
|
819
|
+
# NOT for determining scan scope. Using it caused CSV row deletion issues.
|
|
820
|
+
# Check RAW context config for example_output_path, or default to "context".
|
|
821
|
+
# Do NOT use output_paths_str since generate_output_paths always returns absolute paths.
|
|
822
|
+
example_path_str = None
|
|
823
|
+
if original_context_config:
|
|
824
|
+
example_path_str = original_context_config.get("example_output_path")
|
|
825
|
+
|
|
826
|
+
# Final fallback to "context" (sensible default for this project)
|
|
827
|
+
if not example_path_str:
|
|
828
|
+
example_path_str = "context"
|
|
829
|
+
|
|
830
|
+
# example_path_str can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
547
831
|
# 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
832
|
example_path = Path(example_path_str)
|
|
550
833
|
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
551
834
|
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|
|
@@ -633,7 +916,13 @@ def construct_paths(
|
|
|
633
916
|
|
|
634
917
|
# ------------- Step 2: basename --------------------------
|
|
635
918
|
try:
|
|
636
|
-
|
|
919
|
+
# For sync, example, and test commands, prefer the basename from command_options if provided.
|
|
920
|
+
# This preserves subdirectory paths like 'core/cloud' which would otherwise
|
|
921
|
+
# be lost when extracting from the prompt file path.
|
|
922
|
+
if command in ("sync", "example", "test") and command_options.get("basename"):
|
|
923
|
+
basename = command_options["basename"]
|
|
924
|
+
else:
|
|
925
|
+
basename = _extract_basename(command, input_paths)
|
|
637
926
|
except ValueError as exc:
|
|
638
927
|
# Check if it's the specific error from the initial check (now done at start)
|
|
639
928
|
# This try/except might not be needed if initial check is robust
|
|
@@ -854,9 +1143,22 @@ def construct_paths(
|
|
|
854
1143
|
resolved_config["prompts_dir"] = str(next(iter(input_paths.values())).parent)
|
|
855
1144
|
resolved_config["code_dir"] = str(gen_path.parent)
|
|
856
1145
|
resolved_config["tests_dir"] = str(Path(resolved_config.get("test_output_path", "tests")).parent)
|
|
857
|
-
|
|
1146
|
+
|
|
1147
|
+
# Determine examples_dir for auto-deps scanning
|
|
1148
|
+
# NOTE: outputs.example.path is for OUTPUT only (where to write examples),
|
|
1149
|
+
# NOT for determining scan scope. Using it caused CSV row deletion issues.
|
|
1150
|
+
# Check RAW context config for example_output_path, or default to "context".
|
|
1151
|
+
# Do NOT use resolved_config since generate_output_paths sets it to absolute paths.
|
|
1152
|
+
example_path_str = None
|
|
1153
|
+
if original_context_config:
|
|
1154
|
+
example_path_str = original_context_config.get("example_output_path")
|
|
1155
|
+
|
|
1156
|
+
# Final fallback to "context" (sensible default for this project)
|
|
1157
|
+
if not example_path_str:
|
|
1158
|
+
example_path_str = "context"
|
|
1159
|
+
|
|
1160
|
+
# example_path_str can be a directory (e.g., "context/") or a file path (e.g., "examples/foo.py")
|
|
858
1161
|
# 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
1162
|
example_path = Path(example_path_str)
|
|
861
1163
|
if example_path_str.endswith('/') or '.' not in example_path.name:
|
|
862
1164
|
resolved_config["examples_dir"] = example_path_str.rstrip('/')
|