pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/agentic_update.py CHANGED
@@ -16,6 +16,7 @@ import os
16
16
  import traceback
17
17
 
18
18
  from rich.console import Console
19
+ from rich.markdown import Markdown
19
20
 
20
21
  from .agentic_common import get_available_agents, run_agentic_task
21
22
  from .load_prompt_template import load_prompt_template
@@ -107,17 +108,23 @@ def _detect_changed_files(
107
108
  return sorted({p.resolve() for p in changed})
108
109
 
109
110
 
110
- def _discover_test_files(code_path: Path) -> List[Path]:
111
+ def _discover_test_files(
112
+ code_path: Path,
113
+ tests_dir: Optional[Path] = None,
114
+ ) -> List[Path]:
111
115
  """
112
116
  Discover test files associated with a given code file.
113
117
 
114
118
  Uses pattern: ``test_{code_stem}*{code_suffix}`` and searches in:
115
- 1. ``tests/`` relative to the code file directory
116
- 2. The same directory as the code file
117
- 3. Project root ``tests/``
119
+ 1. Configured tests_dir from .pddrc (if provided)
120
+ 2. ``tests/`` relative to the code file directory
121
+ 3. The same directory as the code file
122
+ 4. Sibling ``tests/`` directory (../tests/)
123
+ 5. Project root ``tests/``
118
124
 
119
125
  Args:
120
126
  code_path: Path to the main code file.
127
+ tests_dir: Optional path to tests directory from .pddrc config.
121
128
 
122
129
  Returns:
123
130
  Ordered list of discovered test file paths (deduplicated).
@@ -127,11 +134,15 @@ def _discover_test_files(code_path: Path) -> List[Path]:
127
134
  suffix = code_path.suffix
128
135
  pattern = f"test_{stem}*{suffix}"
129
136
 
130
- search_dirs: List[Path] = [
137
+ search_dirs: List[Path] = []
138
+ if tests_dir is not None:
139
+ search_dirs.append(Path(tests_dir).resolve())
140
+ search_dirs.extend([
131
141
  code_path.parent / "tests",
132
142
  code_path.parent,
143
+ code_path.parent.parent / "tests", # Sibling tests dir (../tests/)
133
144
  PROJECT_ROOT / "tests",
134
- ]
145
+ ])
135
146
 
136
147
  seen: set[Path] = set()
137
148
  discovered: List[Path] = []
@@ -183,6 +194,7 @@ def run_agentic_update(
183
194
  code_file: str,
184
195
  test_files: Optional[List[Path]] = None,
185
196
  *,
197
+ tests_dir: Optional[Path] = None,
186
198
  verbose: bool = False,
187
199
  quiet: bool = False,
188
200
  ) -> Tuple[bool, str, float, str, List[str]]:
@@ -205,6 +217,8 @@ def run_agentic_update(
205
217
  test files are auto-discovered using the pattern
206
218
  ``test_{code_stem}*{code_suffix}`` in the configured search
207
219
  locations.
220
+ tests_dir: Optional path to tests directory from .pddrc config.
221
+ Used for auto-discovery when test_files is None.
208
222
  verbose: If True, enable verbose logging for the underlying agent task.
209
223
  quiet: If True, suppress informational logging from this function.
210
224
  (Passed through to the agent as well; ``quiet`` takes precedence
@@ -268,7 +282,7 @@ def run_agentic_update(
268
282
  return False, error, 0.0, "", []
269
283
  selected_tests = normalized_tests or []
270
284
  else:
271
- selected_tests = _discover_test_files(code_path)
285
+ selected_tests = _discover_test_files(code_path, tests_dir=tests_dir)
272
286
 
273
287
  # Paths to track *before* running the agent (for mtime comparison)
274
288
  before_paths: set[Path] = {prompt_path.resolve(), code_path.resolve()}
@@ -361,7 +375,10 @@ def run_agentic_update(
361
375
 
362
376
  if not quiet:
363
377
  if success:
364
- console.print(f"[green]{base_msg}[/green]")
378
+ console.print("[green]Prompt file updated successfully.[/green]")
379
+ if output_message:
380
+ console.print("\n[bold]Agent output:[/bold]")
381
+ console.print(Markdown(output_message))
365
382
  else:
366
383
  console.print(f"[yellow]{base_msg}[/yellow]")
367
384
 
@@ -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)