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/load_prompt_template.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
import os
|
|
3
2
|
from typing import Optional
|
|
4
|
-
import sys
|
|
5
3
|
from rich import print
|
|
4
|
+
from pdd.path_resolution import get_default_resolver
|
|
6
5
|
|
|
7
6
|
def print_formatted(message: str) -> None:
|
|
8
7
|
"""Print message with raw formatting tags for testing compatibility."""
|
|
@@ -23,40 +22,22 @@ def load_prompt_template(prompt_name: str) -> Optional[str]:
|
|
|
23
22
|
print_formatted("[red]Unexpected error loading prompt template[/red]")
|
|
24
23
|
return None
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
project_path_env = os.getenv('PDD_PATH')
|
|
29
|
-
candidate_paths = []
|
|
30
|
-
if project_path_env:
|
|
31
|
-
candidate_paths.append(Path(project_path_env))
|
|
25
|
+
resolver = get_default_resolver()
|
|
26
|
+
prompt_path = resolver.resolve_prompt_template(prompt_name)
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# Fallback 2: current working directory
|
|
42
|
-
candidate_paths.append(Path.cwd())
|
|
43
|
-
|
|
44
|
-
# Build candidate prompt paths to try in order
|
|
45
|
-
prompt_candidates = []
|
|
46
|
-
for cp in candidate_paths:
|
|
47
|
-
# Check both <path>/prompts/ and <path>/pdd/prompts/
|
|
48
|
-
# The latter handles installed package case where prompts are in pdd/prompts/
|
|
49
|
-
prompt_candidates.append(cp / 'prompts' / f"{prompt_name}.prompt")
|
|
50
|
-
prompt_candidates.append(cp / 'pdd' / 'prompts' / f"{prompt_name}.prompt")
|
|
28
|
+
if prompt_path is None:
|
|
29
|
+
candidate_roots = []
|
|
30
|
+
if resolver.pdd_path_env is not None:
|
|
31
|
+
candidate_roots.append(resolver.pdd_path_env)
|
|
32
|
+
if resolver.repo_root is not None:
|
|
33
|
+
candidate_roots.append(resolver.repo_root)
|
|
34
|
+
candidate_roots.append(resolver.cwd)
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
prompt_path = candidate
|
|
57
|
-
break
|
|
36
|
+
prompt_candidates = []
|
|
37
|
+
for root in candidate_roots:
|
|
38
|
+
prompt_candidates.append(root / 'prompts' / f"{prompt_name}.prompt")
|
|
39
|
+
prompt_candidates.append(root / 'pdd' / 'prompts' / f"{prompt_name}.prompt")
|
|
58
40
|
|
|
59
|
-
if prompt_path is None:
|
|
60
41
|
tried = "\n".join(str(c) for c in prompt_candidates)
|
|
61
42
|
print_formatted(
|
|
62
43
|
f"[red]Prompt file not found in any candidate locations for '{prompt_name}'. Tried:\n{tried}[/red]"
|
|
@@ -82,4 +63,4 @@ if __name__ == "__main__":
|
|
|
82
63
|
prompt = load_prompt_template("example_prompt")
|
|
83
64
|
if prompt:
|
|
84
65
|
print_formatted("[blue]Loaded prompt template:[/blue]")
|
|
85
|
-
print_formatted(prompt)
|
|
66
|
+
print_formatted(prompt)
|
pdd/operation_log.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
# We assume standard paths relative to the project root
|
|
16
|
+
PDD_DIR = ".pdd"
|
|
17
|
+
META_DIR = os.path.join(PDD_DIR, "meta")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ensure_meta_dir() -> None:
|
|
21
|
+
"""Ensure the .pdd/meta directory exists."""
|
|
22
|
+
os.makedirs(META_DIR, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_log_path(basename: str, language: str) -> Path:
|
|
26
|
+
"""Get the path to the sync log for a specific module."""
|
|
27
|
+
ensure_meta_dir()
|
|
28
|
+
return Path(META_DIR) / f"{basename}_{language}_sync.log"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_fingerprint_path(basename: str, language: str) -> Path:
|
|
32
|
+
"""Get the path to the fingerprint JSON file for a specific module."""
|
|
33
|
+
ensure_meta_dir()
|
|
34
|
+
return Path(META_DIR) / f"{basename}_{language}.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_run_report_path(basename: str, language: str) -> Path:
|
|
38
|
+
"""Get the path to the run report file for a specific module."""
|
|
39
|
+
ensure_meta_dir()
|
|
40
|
+
return Path(META_DIR) / f"{basename}_{language}_run.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def infer_module_identity(prompt_file_path: Union[str, Path]) -> Tuple[Optional[str], Optional[str]]:
|
|
44
|
+
"""
|
|
45
|
+
Infer basename and language from a prompt file path.
|
|
46
|
+
|
|
47
|
+
Expected pattern: prompts/{basename}_{language}.prompt
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
prompt_file_path: Path to the prompt file.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Tuple of (basename, language) or (None, None) if inference fails.
|
|
54
|
+
"""
|
|
55
|
+
path_obj = Path(prompt_file_path)
|
|
56
|
+
filename = path_obj.stem # e.g., "my_module_python" from "my_module_python.prompt"
|
|
57
|
+
|
|
58
|
+
# Try to split by the last underscore to separate language
|
|
59
|
+
# This is a heuristic; strict naming conventions are assumed
|
|
60
|
+
match = re.match(r"^(.*)_([^_]+)$", filename)
|
|
61
|
+
if match:
|
|
62
|
+
basename = match.group(1)
|
|
63
|
+
language = match.group(2)
|
|
64
|
+
return basename, language
|
|
65
|
+
|
|
66
|
+
return None, None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_operation_log(basename: str, language: str) -> List[Dict[str, Any]]:
|
|
70
|
+
"""
|
|
71
|
+
Load all log entries for a module.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
basename: Module basename.
|
|
75
|
+
language: Module language.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of log entries (dictionaries).
|
|
79
|
+
"""
|
|
80
|
+
log_path = get_log_path(basename, language)
|
|
81
|
+
entries = []
|
|
82
|
+
|
|
83
|
+
if log_path.exists():
|
|
84
|
+
try:
|
|
85
|
+
with open(log_path, 'r', encoding='utf-8') as f:
|
|
86
|
+
for line in f:
|
|
87
|
+
if line.strip():
|
|
88
|
+
try:
|
|
89
|
+
entry = json.loads(line)
|
|
90
|
+
# Backwards compatibility: defaulting invocation_mode to "sync"
|
|
91
|
+
if "invocation_mode" not in entry:
|
|
92
|
+
entry["invocation_mode"] = "sync"
|
|
93
|
+
entries.append(entry)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
continue
|
|
96
|
+
except Exception:
|
|
97
|
+
# If log is corrupt or unreadable, return empty list rather than crashing
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
return entries
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def append_log_entry(
|
|
104
|
+
basename: str,
|
|
105
|
+
language: str,
|
|
106
|
+
entry: Dict[str, Any]
|
|
107
|
+
) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Append a single entry to the module's sync log.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
basename: Module basename.
|
|
113
|
+
language: Module language.
|
|
114
|
+
entry: Dictionary of data to log.
|
|
115
|
+
"""
|
|
116
|
+
log_path = get_log_path(basename, language)
|
|
117
|
+
|
|
118
|
+
# Ensure standard fields exist
|
|
119
|
+
if "timestamp" not in entry:
|
|
120
|
+
entry["timestamp"] = datetime.now().isoformat()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with open(log_path, 'a', encoding='utf-8') as f:
|
|
124
|
+
f.write(json.dumps(entry) + "\n")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
# Fallback console warning if logging fails
|
|
127
|
+
console = Console()
|
|
128
|
+
console.print(f"[yellow]Warning: Failed to write to log file {log_path}: {e}[/yellow]")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_log_entry(
|
|
132
|
+
operation: str,
|
|
133
|
+
reason: str,
|
|
134
|
+
invocation_mode: str = "sync",
|
|
135
|
+
estimated_cost: float = 0.0,
|
|
136
|
+
confidence: float = 0.0,
|
|
137
|
+
decision_type: str = "unknown"
|
|
138
|
+
) -> Dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Create a new log entry dictionary structure.
|
|
141
|
+
"""
|
|
142
|
+
return {
|
|
143
|
+
"timestamp": datetime.now().isoformat(),
|
|
144
|
+
"operation": operation,
|
|
145
|
+
"reason": reason,
|
|
146
|
+
"invocation_mode": invocation_mode,
|
|
147
|
+
"estimated_cost": estimated_cost,
|
|
148
|
+
"confidence": confidence,
|
|
149
|
+
"decision_type": decision_type,
|
|
150
|
+
"success": False,
|
|
151
|
+
"duration": 0.0,
|
|
152
|
+
"actual_cost": 0.0,
|
|
153
|
+
"model": "unknown",
|
|
154
|
+
"error": None
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_manual_log_entry(operation: str) -> Dict[str, Any]:
|
|
159
|
+
"""
|
|
160
|
+
Convenience function to create a manual invocation log entry dict.
|
|
161
|
+
"""
|
|
162
|
+
return create_log_entry(
|
|
163
|
+
operation=operation,
|
|
164
|
+
reason="Manual invocation via CLI",
|
|
165
|
+
invocation_mode="manual"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def update_log_entry(
|
|
170
|
+
entry: Dict[str, Any],
|
|
171
|
+
success: bool,
|
|
172
|
+
cost: float,
|
|
173
|
+
model: str,
|
|
174
|
+
duration: float,
|
|
175
|
+
error: Optional[str] = None
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""
|
|
178
|
+
Update a log entry with execution results.
|
|
179
|
+
"""
|
|
180
|
+
entry["success"] = success
|
|
181
|
+
entry["actual_cost"] = cost
|
|
182
|
+
entry["model"] = model
|
|
183
|
+
entry["duration"] = duration
|
|
184
|
+
entry["error"] = error
|
|
185
|
+
return entry
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def log_event(
|
|
189
|
+
basename: str,
|
|
190
|
+
language: str,
|
|
191
|
+
event_type: str,
|
|
192
|
+
details: Any,
|
|
193
|
+
invocation_mode: str = "manual"
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Log a special event to the sync log.
|
|
197
|
+
"""
|
|
198
|
+
entry = {
|
|
199
|
+
"timestamp": datetime.now().isoformat(),
|
|
200
|
+
"type": "event",
|
|
201
|
+
"event_type": event_type,
|
|
202
|
+
"details": details,
|
|
203
|
+
"invocation_mode": invocation_mode
|
|
204
|
+
}
|
|
205
|
+
append_log_entry(basename, language, entry)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def save_fingerprint(
|
|
209
|
+
basename: str,
|
|
210
|
+
language: str,
|
|
211
|
+
operation: str,
|
|
212
|
+
paths: Optional[Dict[str, Path]] = None,
|
|
213
|
+
cost: float = 0.0,
|
|
214
|
+
model: str = "unknown"
|
|
215
|
+
) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Save the current fingerprint/state to the state file.
|
|
218
|
+
|
|
219
|
+
Writes the full Fingerprint dataclass format compatible with read_fingerprint()
|
|
220
|
+
in sync_determine_operation.py. This ensures manual commands (generate, example)
|
|
221
|
+
don't break sync's fingerprint tracking.
|
|
222
|
+
"""
|
|
223
|
+
from dataclasses import asdict
|
|
224
|
+
from datetime import timezone
|
|
225
|
+
from .sync_determine_operation import calculate_current_hashes, Fingerprint
|
|
226
|
+
from . import __version__
|
|
227
|
+
|
|
228
|
+
path = get_fingerprint_path(basename, language)
|
|
229
|
+
|
|
230
|
+
# Calculate file hashes from paths (if provided)
|
|
231
|
+
current_hashes = calculate_current_hashes(paths) if paths else {}
|
|
232
|
+
|
|
233
|
+
# Create Fingerprint with same format as _save_fingerprint_atomic
|
|
234
|
+
fingerprint = Fingerprint(
|
|
235
|
+
pdd_version=__version__,
|
|
236
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
237
|
+
command=operation,
|
|
238
|
+
prompt_hash=current_hashes.get('prompt_hash'),
|
|
239
|
+
code_hash=current_hashes.get('code_hash'),
|
|
240
|
+
example_hash=current_hashes.get('example_hash'),
|
|
241
|
+
test_hash=current_hashes.get('test_hash'),
|
|
242
|
+
test_files=current_hashes.get('test_files'),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
247
|
+
json.dump(asdict(fingerprint), f, indent=2)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
console = Console()
|
|
250
|
+
console.print(f"[yellow]Warning: Failed to save fingerprint to {path}: {e}[/yellow]")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def save_run_report(basename: str, language: str, report_data: Dict[str, Any]) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Save a run report (test results) to the state file.
|
|
256
|
+
"""
|
|
257
|
+
path = get_run_report_path(basename, language)
|
|
258
|
+
try:
|
|
259
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
260
|
+
json.dump(report_data, f, indent=2)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
console = Console()
|
|
263
|
+
console.print(f"[yellow]Warning: Failed to save run report to {path}: {e}[/yellow]")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def clear_run_report(basename: str, language: str) -> None:
|
|
267
|
+
"""
|
|
268
|
+
Remove an existing run report if it exists.
|
|
269
|
+
"""
|
|
270
|
+
path = get_run_report_path(basename, language)
|
|
271
|
+
if path.exists():
|
|
272
|
+
try:
|
|
273
|
+
os.remove(path)
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def log_operation(
|
|
279
|
+
operation: str,
|
|
280
|
+
updates_fingerprint: bool = False,
|
|
281
|
+
updates_run_report: bool = False,
|
|
282
|
+
clears_run_report: bool = False
|
|
283
|
+
) -> Callable:
|
|
284
|
+
"""
|
|
285
|
+
Decorator for Click commands to automatically log operations and manage state.
|
|
286
|
+
"""
|
|
287
|
+
def decorator(func: Callable) -> Callable:
|
|
288
|
+
@functools.wraps(func)
|
|
289
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
290
|
+
# Try to get prompt_file from named kwarg first
|
|
291
|
+
prompt_file = kwargs.get('prompt_file')
|
|
292
|
+
|
|
293
|
+
# If not found, check if there's an 'args' tuple (for commands using nargs=-1)
|
|
294
|
+
# and the first element looks like a prompt file path
|
|
295
|
+
if not prompt_file:
|
|
296
|
+
cli_args = kwargs.get('args')
|
|
297
|
+
if cli_args and len(cli_args) > 0:
|
|
298
|
+
first_arg = str(cli_args[0])
|
|
299
|
+
# Check if it looks like a prompt file (ends with .prompt)
|
|
300
|
+
if first_arg.endswith('.prompt'):
|
|
301
|
+
prompt_file = first_arg
|
|
302
|
+
|
|
303
|
+
basename, language = (None, None)
|
|
304
|
+
if prompt_file:
|
|
305
|
+
basename, language = infer_module_identity(prompt_file)
|
|
306
|
+
|
|
307
|
+
if basename and language and clears_run_report:
|
|
308
|
+
clear_run_report(basename, language)
|
|
309
|
+
|
|
310
|
+
entry = create_manual_log_entry(operation=operation)
|
|
311
|
+
start_time = time.time()
|
|
312
|
+
success = False
|
|
313
|
+
result = None
|
|
314
|
+
error_msg = None
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
result = func(*args, **kwargs)
|
|
318
|
+
success = True
|
|
319
|
+
return result
|
|
320
|
+
except Exception as e:
|
|
321
|
+
success = False
|
|
322
|
+
error_msg = str(e)
|
|
323
|
+
raise
|
|
324
|
+
finally:
|
|
325
|
+
duration = time.time() - start_time
|
|
326
|
+
cost = 0.0
|
|
327
|
+
model = "unknown"
|
|
328
|
+
if success and result:
|
|
329
|
+
if isinstance(result, tuple) and len(result) >= 3:
|
|
330
|
+
if isinstance(result[1], (int, float)): cost = float(result[1])
|
|
331
|
+
if isinstance(result[2], str): model = str(result[2])
|
|
332
|
+
|
|
333
|
+
update_log_entry(entry, success=success, cost=cost, model=model, duration=duration, error=error_msg)
|
|
334
|
+
if basename and language:
|
|
335
|
+
append_log_entry(basename, language, entry)
|
|
336
|
+
if success:
|
|
337
|
+
if updates_fingerprint:
|
|
338
|
+
save_fingerprint(basename, language, operation=operation, cost=cost, model=model)
|
|
339
|
+
if updates_run_report and isinstance(result, dict):
|
|
340
|
+
save_run_report(basename, language, result)
|
|
341
|
+
return wrapper
|
|
342
|
+
return decorator
|
pdd/path_resolution.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
IncludeProfile = Literal["cwd_then_package_then_repo"]
|
|
9
|
+
PromptProfile = Literal["pdd_path_then_repo_then_cwd"]
|
|
10
|
+
DataProfile = Literal["pdd_path_only"]
|
|
11
|
+
ProjectRootProfile = Literal["pdd_path_then_marker_then_cwd"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class PathResolver:
|
|
16
|
+
cwd: Path
|
|
17
|
+
pdd_path_env: Optional[Path]
|
|
18
|
+
package_root: Path
|
|
19
|
+
repo_root: Optional[Path]
|
|
20
|
+
|
|
21
|
+
def resolve_include(self, rel: str, profile: IncludeProfile = "cwd_then_package_then_repo") -> Path:
|
|
22
|
+
if profile != "cwd_then_package_then_repo":
|
|
23
|
+
raise ValueError(f"Unsupported include profile: {profile}")
|
|
24
|
+
|
|
25
|
+
cwd_path = self.cwd / rel
|
|
26
|
+
if cwd_path.exists():
|
|
27
|
+
return cwd_path
|
|
28
|
+
|
|
29
|
+
pkg_path = self.package_root / rel
|
|
30
|
+
if pkg_path.exists():
|
|
31
|
+
return pkg_path
|
|
32
|
+
|
|
33
|
+
if self.repo_root is not None:
|
|
34
|
+
repo_path = self.repo_root / rel
|
|
35
|
+
if repo_path.exists():
|
|
36
|
+
return repo_path
|
|
37
|
+
|
|
38
|
+
return cwd_path
|
|
39
|
+
|
|
40
|
+
def resolve_prompt_template(
|
|
41
|
+
self,
|
|
42
|
+
name: str,
|
|
43
|
+
profile: PromptProfile = "pdd_path_then_repo_then_cwd",
|
|
44
|
+
) -> Optional[Path]:
|
|
45
|
+
if profile != "pdd_path_then_repo_then_cwd":
|
|
46
|
+
raise ValueError(f"Unsupported prompt profile: {profile}")
|
|
47
|
+
|
|
48
|
+
roots = []
|
|
49
|
+
if self.pdd_path_env is not None:
|
|
50
|
+
roots.append(self.pdd_path_env)
|
|
51
|
+
if self.repo_root is not None:
|
|
52
|
+
roots.append(self.repo_root)
|
|
53
|
+
roots.append(self.cwd)
|
|
54
|
+
|
|
55
|
+
prompt_file = f"{name}.prompt"
|
|
56
|
+
for root in roots:
|
|
57
|
+
candidate = root / "prompts" / prompt_file
|
|
58
|
+
if candidate.exists():
|
|
59
|
+
return candidate
|
|
60
|
+
candidate = root / "pdd" / "prompts" / prompt_file
|
|
61
|
+
if candidate.exists():
|
|
62
|
+
return candidate
|
|
63
|
+
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def resolve_data_file(self, rel: str, profile: DataProfile = "pdd_path_only") -> Path:
|
|
67
|
+
if profile != "pdd_path_only":
|
|
68
|
+
raise ValueError(f"Unsupported data profile: {profile}")
|
|
69
|
+
if self.pdd_path_env is None:
|
|
70
|
+
raise ValueError("PDD_PATH environment variable is not set.")
|
|
71
|
+
return self.pdd_path_env / rel
|
|
72
|
+
|
|
73
|
+
def resolve_project_root(
|
|
74
|
+
self,
|
|
75
|
+
profile: ProjectRootProfile = "pdd_path_then_marker_then_cwd",
|
|
76
|
+
max_levels: int = 5,
|
|
77
|
+
) -> Path:
|
|
78
|
+
if profile != "pdd_path_then_marker_then_cwd":
|
|
79
|
+
raise ValueError(f"Unsupported project root profile: {profile}")
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
self.pdd_path_env is not None
|
|
83
|
+
and self.pdd_path_env.is_dir()
|
|
84
|
+
and not _is_within(self.pdd_path_env, self.package_root)
|
|
85
|
+
):
|
|
86
|
+
return self.pdd_path_env
|
|
87
|
+
|
|
88
|
+
current = self.cwd
|
|
89
|
+
for _ in range(max_levels):
|
|
90
|
+
if _has_project_marker(current):
|
|
91
|
+
return current
|
|
92
|
+
parent = current.parent
|
|
93
|
+
if parent == current:
|
|
94
|
+
break
|
|
95
|
+
current = parent
|
|
96
|
+
|
|
97
|
+
return self.cwd
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_default_resolver() -> PathResolver:
|
|
101
|
+
cwd = Path.cwd().resolve()
|
|
102
|
+
|
|
103
|
+
pdd_path_env = None
|
|
104
|
+
env_value = os.getenv("PDD_PATH")
|
|
105
|
+
if env_value:
|
|
106
|
+
pdd_path_env = Path(env_value).expanduser().resolve()
|
|
107
|
+
|
|
108
|
+
package_root = Path(__file__).resolve().parent
|
|
109
|
+
repo_root = package_root.parent
|
|
110
|
+
|
|
111
|
+
return PathResolver(
|
|
112
|
+
cwd=cwd,
|
|
113
|
+
pdd_path_env=pdd_path_env,
|
|
114
|
+
package_root=package_root,
|
|
115
|
+
repo_root=repo_root,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _has_project_marker(path: Path) -> bool:
|
|
120
|
+
return (
|
|
121
|
+
(path / ".git").exists()
|
|
122
|
+
or (path / "pyproject.toml").exists()
|
|
123
|
+
or (path / "data").is_dir()
|
|
124
|
+
or (path / ".env").exists()
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _is_within(path: Path, parent: Path) -> bool:
|
|
129
|
+
try:
|
|
130
|
+
resolved_path = path.resolve()
|
|
131
|
+
resolved_parent = parent.resolve()
|
|
132
|
+
except Exception:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
if resolved_path == resolved_parent:
|
|
136
|
+
return True
|
|
137
|
+
parent_str = str(resolved_parent)
|
|
138
|
+
if not parent_str.endswith(os.sep):
|
|
139
|
+
parent_str = parent_str + os.sep
|
|
140
|
+
return str(resolved_path).startswith(parent_str)
|