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/architecture_sync.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Architecture sync module for bidirectional sync between architecture.json and prompt files.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to:
|
|
5
|
+
1. Parse PDD metadata tags (<pdd-reason>, <pdd-interface>, <pdd-dependency>) from prompt files
|
|
6
|
+
2. Update architecture.json from prompt file tags (prompt → architecture.json)
|
|
7
|
+
3. Validate dependencies and detect issues
|
|
8
|
+
|
|
9
|
+
Philosophy: Prompts are the source of truth, architecture.json is derived from prompts.
|
|
10
|
+
Validation: Lenient - missing tags are OK, only update fields that have tags present.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from lxml import etree
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- Constants ---
|
|
21
|
+
# Use Path.cwd() instead of __file__ so it works with the user's project directory,
|
|
22
|
+
# not the PDD package installation directory
|
|
23
|
+
ARCHITECTURE_JSON_PATH = Path.cwd() / "architecture.json"
|
|
24
|
+
PROMPTS_DIR = Path.cwd() / "prompts"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- Tag Extraction ---
|
|
28
|
+
|
|
29
|
+
def parse_prompt_tags(prompt_content: str) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Extract PDD metadata tags from prompt content using lxml.
|
|
32
|
+
|
|
33
|
+
Extracts the following tags:
|
|
34
|
+
- <pdd-reason>: Brief description of module's purpose
|
|
35
|
+
- <pdd-interface>: JSON interface specification
|
|
36
|
+
- <pdd-dependency>: Module dependencies (multiple tags allowed)
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
prompt_content: Raw content of .prompt file
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict with keys:
|
|
43
|
+
- reason: str | None (single line description)
|
|
44
|
+
- interface: dict | None (parsed JSON interface structure)
|
|
45
|
+
- dependencies: List[str] (prompt filenames, empty if none)
|
|
46
|
+
|
|
47
|
+
Lenient behavior:
|
|
48
|
+
- Malformed XML: Returns empty dict without crashing
|
|
49
|
+
- Invalid JSON in interface: Returns None for interface, continues with other fields
|
|
50
|
+
- Missing tags: Returns None/empty for missing fields
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> content = '''
|
|
54
|
+
... <pdd-reason>Provides unified LLM invocation</pdd-reason>
|
|
55
|
+
... <pdd-interface>{"type": "module", "module": {"functions": []}}</pdd-interface>
|
|
56
|
+
... <pdd-dependency>path_resolution_python.prompt</pdd-dependency>
|
|
57
|
+
... '''
|
|
58
|
+
>>> result = parse_prompt_tags(content)
|
|
59
|
+
>>> result['reason']
|
|
60
|
+
'Provides unified LLM invocation'
|
|
61
|
+
>>> result['dependencies']
|
|
62
|
+
['path_resolution_python.prompt']
|
|
63
|
+
"""
|
|
64
|
+
result = {
|
|
65
|
+
'reason': None,
|
|
66
|
+
'interface': None,
|
|
67
|
+
'dependencies': [],
|
|
68
|
+
'has_dependency_tags': False, # Track if <pdd-dependency> tags were present
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Wrap content in root element for XML parsing
|
|
73
|
+
# Replace CDATA markers if present to handle JSON with special chars
|
|
74
|
+
xml_content = f"<root>{prompt_content}</root>"
|
|
75
|
+
|
|
76
|
+
# Parse with lxml (lenient on encoding)
|
|
77
|
+
parser = etree.XMLParser(recover=True) # Lenient parser
|
|
78
|
+
root = etree.fromstring(xml_content.encode('utf-8'), parser=parser)
|
|
79
|
+
|
|
80
|
+
# Extract <pdd-reason>
|
|
81
|
+
reason_elem = root.find('.//pdd-reason')
|
|
82
|
+
if reason_elem is not None and reason_elem.text:
|
|
83
|
+
result['reason'] = reason_elem.text.strip()
|
|
84
|
+
|
|
85
|
+
# Extract <pdd-interface> (parse as JSON)
|
|
86
|
+
interface_elem = root.find('.//pdd-interface')
|
|
87
|
+
if interface_elem is not None and interface_elem.text:
|
|
88
|
+
try:
|
|
89
|
+
interface_text = interface_elem.text.strip()
|
|
90
|
+
result['interface'] = json.loads(interface_text)
|
|
91
|
+
except json.JSONDecodeError as e:
|
|
92
|
+
# Invalid JSON, skip interface field (lenient) but store warning
|
|
93
|
+
result['interface_parse_error'] = f"Invalid JSON in <pdd-interface>: {str(e)}"
|
|
94
|
+
|
|
95
|
+
# Extract <pdd-dependency> tags (multiple allowed)
|
|
96
|
+
dep_elems = root.findall('.//pdd-dependency')
|
|
97
|
+
# Track if any dependency tags were present (even if empty)
|
|
98
|
+
# This distinguishes "no tags" (don't update) from "tags removed" (update to empty)
|
|
99
|
+
result['has_dependency_tags'] = len(dep_elems) > 0 or '<pdd-dependency>' in prompt_content
|
|
100
|
+
result['dependencies'] = [
|
|
101
|
+
elem.text.strip()
|
|
102
|
+
for elem in dep_elems
|
|
103
|
+
if elem.text and elem.text.strip()
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
except (etree.XMLSyntaxError, etree.ParserError):
|
|
107
|
+
# Malformed XML, return empty result (lenient)
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --- Architecture Update ---
|
|
114
|
+
|
|
115
|
+
def update_architecture_from_prompt(
|
|
116
|
+
prompt_filename: str,
|
|
117
|
+
prompts_dir: Path = PROMPTS_DIR,
|
|
118
|
+
architecture_path: Path = ARCHITECTURE_JSON_PATH,
|
|
119
|
+
dry_run: bool = False
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Update a single architecture.json entry from its prompt file tags.
|
|
123
|
+
|
|
124
|
+
This function:
|
|
125
|
+
1. Reads the prompt file and extracts PDD metadata tags
|
|
126
|
+
2. Loads architecture.json and finds the matching module entry
|
|
127
|
+
3. Updates only fields that have tags present (lenient)
|
|
128
|
+
4. Tracks changes for diff display
|
|
129
|
+
5. Writes back to architecture.json (unless dry_run=True)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
prompt_filename: Name of prompt file (e.g., "llm_invoke_python.prompt")
|
|
133
|
+
prompts_dir: Directory containing prompt files (default: ./prompts/)
|
|
134
|
+
architecture_path: Path to architecture.json (default: ./architecture.json)
|
|
135
|
+
dry_run: If True, return changes without writing to file
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dict with keys:
|
|
139
|
+
- success: bool (True if operation succeeded)
|
|
140
|
+
- updated: bool (True if any fields changed)
|
|
141
|
+
- changes: Dict mapping field names to {'old': ..., 'new': ...}
|
|
142
|
+
- error: Optional[str] (error message if success=False)
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> result = update_architecture_from_prompt("llm_invoke_python.prompt")
|
|
146
|
+
>>> if result['success'] and result['updated']:
|
|
147
|
+
... print(f"Updated fields: {list(result['changes'].keys())}")
|
|
148
|
+
Updated fields: ['reason', 'dependencies']
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
# 1. Read prompt file
|
|
152
|
+
prompt_path = prompts_dir / prompt_filename
|
|
153
|
+
if not prompt_path.exists():
|
|
154
|
+
return {
|
|
155
|
+
'success': False,
|
|
156
|
+
'updated': False,
|
|
157
|
+
'changes': {},
|
|
158
|
+
'error': f'Prompt file not found: {prompt_filename}'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
prompt_content = prompt_path.read_text(encoding='utf-8')
|
|
162
|
+
|
|
163
|
+
# 2. Extract tags
|
|
164
|
+
tags = parse_prompt_tags(prompt_content)
|
|
165
|
+
|
|
166
|
+
# 3. Load architecture.json
|
|
167
|
+
if not architecture_path.exists():
|
|
168
|
+
return {
|
|
169
|
+
'success': False,
|
|
170
|
+
'updated': False,
|
|
171
|
+
'changes': {},
|
|
172
|
+
'error': f'Architecture file not found: {architecture_path}'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
arch_data = json.loads(architecture_path.read_text(encoding='utf-8'))
|
|
176
|
+
|
|
177
|
+
# 4. Find matching module by filename
|
|
178
|
+
module_entry = None
|
|
179
|
+
module_index = None
|
|
180
|
+
for i, mod in enumerate(arch_data):
|
|
181
|
+
if mod.get('filename') == prompt_filename:
|
|
182
|
+
module_entry = mod
|
|
183
|
+
module_index = i
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
if module_entry is None:
|
|
187
|
+
return {
|
|
188
|
+
'success': False,
|
|
189
|
+
'updated': False,
|
|
190
|
+
'changes': {},
|
|
191
|
+
'error': f'No architecture entry found for: {prompt_filename}'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# 5. Track changes (only update fields with tags present - lenient)
|
|
195
|
+
changes = {}
|
|
196
|
+
updated = False
|
|
197
|
+
|
|
198
|
+
# Check if prompt has ANY PDD tags (used to determine if dependencies should be cleared)
|
|
199
|
+
has_any_pdd_tags = (
|
|
200
|
+
tags['reason'] is not None or
|
|
201
|
+
tags['interface'] is not None or
|
|
202
|
+
tags.get('has_dependency_tags', False) or
|
|
203
|
+
tags['dependencies']
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Update reason if tag present
|
|
207
|
+
if tags['reason'] is not None:
|
|
208
|
+
old_reason = module_entry.get('reason')
|
|
209
|
+
if old_reason != tags['reason']:
|
|
210
|
+
changes['reason'] = {'old': old_reason, 'new': tags['reason']}
|
|
211
|
+
module_entry['reason'] = tags['reason']
|
|
212
|
+
updated = True
|
|
213
|
+
|
|
214
|
+
# Update interface if tag present
|
|
215
|
+
if tags['interface'] is not None:
|
|
216
|
+
old_interface = module_entry.get('interface')
|
|
217
|
+
if old_interface != tags['interface']:
|
|
218
|
+
changes['interface'] = {'old': old_interface, 'new': tags['interface']}
|
|
219
|
+
module_entry['interface'] = tags['interface']
|
|
220
|
+
updated = True
|
|
221
|
+
|
|
222
|
+
# Update dependencies if:
|
|
223
|
+
# 1. Dependency tags were present in the prompt (even if empty), OR
|
|
224
|
+
# 2. Prompt has OTHER pdd tags (reason/interface) but no dependency tags = clear dependencies
|
|
225
|
+
# This ensures removing all <pdd-dependency> tags will clear dependencies in architecture.json
|
|
226
|
+
should_update_deps = (
|
|
227
|
+
tags.get('has_dependency_tags', False) or # Has dependency tags (even if empty)
|
|
228
|
+
tags['dependencies'] or # Has dependencies
|
|
229
|
+
has_any_pdd_tags # Has any PDD tags = manage all fields including deps
|
|
230
|
+
)
|
|
231
|
+
if should_update_deps:
|
|
232
|
+
old_deps = module_entry.get('dependencies', [])
|
|
233
|
+
# Compare as sets to detect changes (order-independent)
|
|
234
|
+
if set(old_deps) != set(tags['dependencies']):
|
|
235
|
+
changes['dependencies'] = {'old': old_deps, 'new': tags['dependencies']}
|
|
236
|
+
module_entry['dependencies'] = tags['dependencies']
|
|
237
|
+
updated = True
|
|
238
|
+
|
|
239
|
+
# 6. Write back to architecture.json (if updated and not dry run)
|
|
240
|
+
if updated and not dry_run:
|
|
241
|
+
arch_data[module_index] = module_entry
|
|
242
|
+
architecture_path.write_text(
|
|
243
|
+
json.dumps(arch_data, indent=2, ensure_ascii=False) + '\n',
|
|
244
|
+
encoding='utf-8'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Include any parse warnings
|
|
248
|
+
warnings = []
|
|
249
|
+
if tags.get('interface_parse_error'):
|
|
250
|
+
warnings.append(tags['interface_parse_error'])
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
'success': True,
|
|
254
|
+
'updated': updated,
|
|
255
|
+
'changes': changes,
|
|
256
|
+
'error': None,
|
|
257
|
+
'warnings': warnings
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
return {
|
|
262
|
+
'success': False,
|
|
263
|
+
'updated': False,
|
|
264
|
+
'changes': {},
|
|
265
|
+
'error': f'Unexpected error: {str(e)}'
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def sync_all_prompts_to_architecture(
|
|
270
|
+
prompts_dir: Path = PROMPTS_DIR,
|
|
271
|
+
architecture_path: Path = ARCHITECTURE_JSON_PATH,
|
|
272
|
+
dry_run: bool = False
|
|
273
|
+
) -> Dict[str, Any]:
|
|
274
|
+
"""
|
|
275
|
+
Sync ALL prompt files to architecture.json.
|
|
276
|
+
|
|
277
|
+
Iterates through all modules in architecture.json and updates each from
|
|
278
|
+
its corresponding prompt file (if it exists and has tags).
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
prompts_dir: Directory containing prompt files
|
|
282
|
+
architecture_path: Path to architecture.json
|
|
283
|
+
dry_run: If True, perform validation without writing changes
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict with keys:
|
|
287
|
+
- success: bool (True if no errors occurred)
|
|
288
|
+
- updated_count: int (number of modules updated)
|
|
289
|
+
- skipped_count: int (number of modules without prompt files)
|
|
290
|
+
- results: List[Dict] (detailed results for each module)
|
|
291
|
+
- errors: List[str] (error messages)
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> result = sync_all_prompts_to_architecture(dry_run=True)
|
|
295
|
+
>>> print(f"Would update {result['updated_count']} modules")
|
|
296
|
+
Would update 15 modules
|
|
297
|
+
"""
|
|
298
|
+
# Load architecture.json to get all prompt filenames
|
|
299
|
+
if not architecture_path.exists():
|
|
300
|
+
return {
|
|
301
|
+
'success': False,
|
|
302
|
+
'updated_count': 0,
|
|
303
|
+
'skipped_count': 0,
|
|
304
|
+
'results': [],
|
|
305
|
+
'errors': [f'Architecture file not found: {architecture_path}']
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
arch_data = json.loads(architecture_path.read_text(encoding='utf-8'))
|
|
309
|
+
|
|
310
|
+
results = []
|
|
311
|
+
errors = []
|
|
312
|
+
updated_count = 0
|
|
313
|
+
skipped_count = 0
|
|
314
|
+
|
|
315
|
+
for module in arch_data:
|
|
316
|
+
filename = module.get('filename')
|
|
317
|
+
|
|
318
|
+
# Skip entries without filename or non-prompt files
|
|
319
|
+
if not filename or not filename.endswith('.prompt'):
|
|
320
|
+
skipped_count += 1
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
# Update from prompt
|
|
324
|
+
result = update_architecture_from_prompt(
|
|
325
|
+
filename,
|
|
326
|
+
prompts_dir,
|
|
327
|
+
architecture_path,
|
|
328
|
+
dry_run
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Track statistics
|
|
332
|
+
if result['success'] and result['updated']:
|
|
333
|
+
updated_count += 1
|
|
334
|
+
elif not result['success']:
|
|
335
|
+
errors.append(f"{filename}: {result['error']}")
|
|
336
|
+
|
|
337
|
+
# Store detailed result
|
|
338
|
+
results.append({
|
|
339
|
+
'filename': filename,
|
|
340
|
+
'success': result['success'],
|
|
341
|
+
'updated': result['updated'],
|
|
342
|
+
'changes': result['changes'],
|
|
343
|
+
'error': result.get('error')
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
'success': len(errors) == 0,
|
|
348
|
+
'updated_count': updated_count,
|
|
349
|
+
'skipped_count': skipped_count,
|
|
350
|
+
'results': results,
|
|
351
|
+
'errors': errors
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# --- Validation ---
|
|
356
|
+
|
|
357
|
+
def validate_dependencies(
|
|
358
|
+
dependencies: List[str],
|
|
359
|
+
prompts_dir: Path = PROMPTS_DIR
|
|
360
|
+
) -> Dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Validate dependency list for a module.
|
|
363
|
+
|
|
364
|
+
Checks:
|
|
365
|
+
1. All referenced prompt files exist in prompts_dir
|
|
366
|
+
2. No duplicate dependencies
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
dependencies: List of prompt filenames (e.g., ["llm_invoke_python.prompt"])
|
|
370
|
+
prompts_dir: Directory to check for prompt file existence
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Dict with keys:
|
|
374
|
+
- valid: bool (True if all validations pass)
|
|
375
|
+
- missing: List[str] (prompt files that don't exist)
|
|
376
|
+
- duplicates: List[str] (duplicate entries)
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> deps = ["llm_invoke_python.prompt", "missing.prompt"]
|
|
380
|
+
>>> result = validate_dependencies(deps)
|
|
381
|
+
>>> result['valid']
|
|
382
|
+
False
|
|
383
|
+
>>> result['missing']
|
|
384
|
+
['missing.prompt']
|
|
385
|
+
"""
|
|
386
|
+
missing = []
|
|
387
|
+
duplicates = []
|
|
388
|
+
|
|
389
|
+
# Check for missing files
|
|
390
|
+
for dep in dependencies:
|
|
391
|
+
dep_path = prompts_dir / dep
|
|
392
|
+
if not dep_path.exists():
|
|
393
|
+
missing.append(dep)
|
|
394
|
+
|
|
395
|
+
# Check for duplicates
|
|
396
|
+
seen = set()
|
|
397
|
+
for dep in dependencies:
|
|
398
|
+
if dep in seen:
|
|
399
|
+
if dep not in duplicates: # Avoid duplicate duplicates
|
|
400
|
+
duplicates.append(dep)
|
|
401
|
+
seen.add(dep)
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
'valid': len(missing) == 0 and len(duplicates) == 0,
|
|
405
|
+
'missing': missing,
|
|
406
|
+
'duplicates': duplicates
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def validate_interface_structure(interface: Dict[str, Any]) -> Dict[str, Any]:
|
|
411
|
+
"""
|
|
412
|
+
Validate interface JSON structure.
|
|
413
|
+
|
|
414
|
+
Interface must have:
|
|
415
|
+
- 'type' field with value: 'module' | 'cli' | 'command' | 'frontend'
|
|
416
|
+
- Corresponding nested object with appropriate structure
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
interface: Parsed interface JSON dict
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Dict with keys:
|
|
423
|
+
- valid: bool (True if structure is valid)
|
|
424
|
+
- errors: List[str] (validation error messages)
|
|
425
|
+
|
|
426
|
+
Example:
|
|
427
|
+
>>> interface = {"type": "module", "module": {"functions": []}}
|
|
428
|
+
>>> result = validate_interface_structure(interface)
|
|
429
|
+
>>> result['valid']
|
|
430
|
+
True
|
|
431
|
+
"""
|
|
432
|
+
errors = []
|
|
433
|
+
|
|
434
|
+
if not isinstance(interface, dict):
|
|
435
|
+
return {'valid': False, 'errors': ['Interface must be a JSON object']}
|
|
436
|
+
|
|
437
|
+
# Check type field
|
|
438
|
+
itype = interface.get('type')
|
|
439
|
+
if itype not in ['module', 'cli', 'command', 'frontend']:
|
|
440
|
+
errors.append(f"Invalid type: '{itype}'. Must be: module, cli, command, or frontend")
|
|
441
|
+
return {'valid': False, 'errors': errors}
|
|
442
|
+
|
|
443
|
+
# Check corresponding nested key exists
|
|
444
|
+
if itype not in interface:
|
|
445
|
+
errors.append(f"Missing '{itype}' key for type='{itype}'")
|
|
446
|
+
return {'valid': False, 'errors': errors}
|
|
447
|
+
|
|
448
|
+
# Type-specific validation
|
|
449
|
+
nested_obj = interface[itype]
|
|
450
|
+
if not isinstance(nested_obj, dict):
|
|
451
|
+
errors.append(f"'{itype}' must be an object")
|
|
452
|
+
|
|
453
|
+
if itype == 'module':
|
|
454
|
+
if 'functions' not in nested_obj:
|
|
455
|
+
errors.append("module.functions is required")
|
|
456
|
+
elif itype in ['cli', 'command']:
|
|
457
|
+
if 'commands' not in nested_obj:
|
|
458
|
+
errors.append(f"{itype}.commands is required")
|
|
459
|
+
elif itype == 'frontend':
|
|
460
|
+
if 'pages' not in nested_obj:
|
|
461
|
+
errors.append("frontend.pages is required")
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
'valid': len(errors) == 0,
|
|
465
|
+
'errors': errors
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# --- Helper Functions for Reverse Direction (architecture.json → prompt) ---
|
|
470
|
+
|
|
471
|
+
def get_architecture_entry_for_prompt(
|
|
472
|
+
prompt_filename: str,
|
|
473
|
+
architecture_path: Path = ARCHITECTURE_JSON_PATH
|
|
474
|
+
) -> Optional[Dict[str, Any]]:
|
|
475
|
+
"""
|
|
476
|
+
Load architecture entry matching a prompt filename.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
prompt_filename: Name of prompt file (e.g., "llm_invoke_python.prompt")
|
|
480
|
+
architecture_path: Path to architecture.json
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Architecture entry dict or None if not found
|
|
484
|
+
|
|
485
|
+
Example:
|
|
486
|
+
>>> entry = get_architecture_entry_for_prompt("llm_invoke_python.prompt")
|
|
487
|
+
>>> entry['reason']
|
|
488
|
+
'Provides unified LLM invocation across all PDD operations.'
|
|
489
|
+
"""
|
|
490
|
+
if not architecture_path.exists():
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
arch_data = json.loads(architecture_path.read_text(encoding='utf-8'))
|
|
494
|
+
|
|
495
|
+
# Extract just filename if full path provided
|
|
496
|
+
filename = Path(prompt_filename).name
|
|
497
|
+
|
|
498
|
+
for entry in arch_data:
|
|
499
|
+
if entry.get('filename') == filename:
|
|
500
|
+
return entry
|
|
501
|
+
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def has_pdd_tags(prompt_content: str) -> bool:
|
|
506
|
+
"""
|
|
507
|
+
Check if prompt already has PDD metadata tags.
|
|
508
|
+
|
|
509
|
+
Used to preserve manual edits - don't inject tags if they already exist.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
prompt_content: Raw prompt file content
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
True if any PDD tags are present
|
|
516
|
+
|
|
517
|
+
Example:
|
|
518
|
+
>>> has_pdd_tags("<pdd-reason>Test</pdd-reason>")
|
|
519
|
+
True
|
|
520
|
+
>>> has_pdd_tags("No tags here")
|
|
521
|
+
False
|
|
522
|
+
"""
|
|
523
|
+
return (
|
|
524
|
+
'<pdd-reason>' in prompt_content or
|
|
525
|
+
'<pdd-interface>' in prompt_content or
|
|
526
|
+
'<pdd-dependency>' in prompt_content
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def generate_tags_from_architecture(arch_entry: Dict[str, Any]) -> str:
|
|
531
|
+
"""
|
|
532
|
+
Generate XML tags string from architecture entry.
|
|
533
|
+
|
|
534
|
+
Used when generating new prompts - inject tags from architecture.json.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
arch_entry: Architecture.json module entry
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
XML tags as string (ready to prepend to prompt)
|
|
541
|
+
|
|
542
|
+
Example:
|
|
543
|
+
>>> entry = {"reason": "Test module", "dependencies": ["dep.prompt"]}
|
|
544
|
+
>>> tags = generate_tags_from_architecture(entry)
|
|
545
|
+
>>> print(tags)
|
|
546
|
+
<pdd-reason>Test module</pdd-reason>
|
|
547
|
+
<pdd-dependency>dep.prompt</pdd-dependency>
|
|
548
|
+
"""
|
|
549
|
+
tags = []
|
|
550
|
+
|
|
551
|
+
# Generate <pdd-reason> tag
|
|
552
|
+
if arch_entry.get('reason'):
|
|
553
|
+
reason = arch_entry['reason']
|
|
554
|
+
tags.append(f"<pdd-reason>{reason}</pdd-reason>")
|
|
555
|
+
|
|
556
|
+
# Generate <pdd-interface> tag (pretty-printed JSON)
|
|
557
|
+
if arch_entry.get('interface'):
|
|
558
|
+
interface_json = json.dumps(arch_entry['interface'], indent=2)
|
|
559
|
+
tags.append(f"<pdd-interface>\n{interface_json}\n</pdd-interface>")
|
|
560
|
+
|
|
561
|
+
# Generate <pdd-dependency> tags (one per dependency)
|
|
562
|
+
for dep in arch_entry.get('dependencies', []):
|
|
563
|
+
tags.append(f"<pdd-dependency>{dep}</pdd-dependency>")
|
|
564
|
+
|
|
565
|
+
return '\n'.join(tags)
|