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.
Files changed (151) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +506 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +537 -0
  6. pdd/agentic_common.py +533 -770
  7. pdd/agentic_crash.py +2 -1
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +582 -0
  10. pdd/agentic_fix.py +118 -3
  11. pdd/agentic_update.py +27 -9
  12. pdd/agentic_verify.py +3 -2
  13. pdd/architecture_sync.py +565 -0
  14. pdd/auth_service.py +210 -0
  15. pdd/auto_deps_main.py +63 -53
  16. pdd/auto_include.py +236 -3
  17. pdd/auto_update.py +125 -47
  18. pdd/bug_main.py +195 -23
  19. pdd/cmd_test_main.py +345 -197
  20. pdd/code_generator.py +4 -2
  21. pdd/code_generator_main.py +118 -32
  22. pdd/commands/__init__.py +6 -0
  23. pdd/commands/analysis.py +113 -48
  24. pdd/commands/auth.py +309 -0
  25. pdd/commands/connect.py +358 -0
  26. pdd/commands/fix.py +155 -114
  27. pdd/commands/generate.py +5 -0
  28. pdd/commands/maintenance.py +3 -2
  29. pdd/commands/misc.py +8 -0
  30. pdd/commands/modify.py +225 -163
  31. pdd/commands/sessions.py +284 -0
  32. pdd/commands/utility.py +12 -7
  33. pdd/construct_paths.py +334 -32
  34. pdd/context_generator_main.py +167 -170
  35. pdd/continue_generation.py +6 -3
  36. pdd/core/__init__.py +33 -0
  37. pdd/core/cli.py +44 -7
  38. pdd/core/cloud.py +237 -0
  39. pdd/core/dump.py +68 -20
  40. pdd/core/errors.py +4 -0
  41. pdd/core/remote_session.py +61 -0
  42. pdd/crash_main.py +219 -23
  43. pdd/data/llm_model.csv +4 -4
  44. pdd/docs/prompting_guide.md +864 -0
  45. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  46. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  47. pdd/fix_code_loop.py +208 -34
  48. pdd/fix_code_module_errors.py +6 -2
  49. pdd/fix_error_loop.py +291 -38
  50. pdd/fix_main.py +208 -6
  51. pdd/fix_verification_errors_loop.py +235 -26
  52. pdd/fix_verification_main.py +269 -83
  53. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  54. pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
  55. pdd/frontend/dist/index.html +376 -0
  56. pdd/frontend/dist/logo.svg +33 -0
  57. pdd/generate_output_paths.py +46 -5
  58. pdd/generate_test.py +212 -151
  59. pdd/get_comment.py +19 -44
  60. pdd/get_extension.py +8 -9
  61. pdd/get_jwt_token.py +309 -20
  62. pdd/get_language.py +8 -7
  63. pdd/get_run_command.py +7 -5
  64. pdd/insert_includes.py +2 -1
  65. pdd/llm_invoke.py +531 -97
  66. pdd/load_prompt_template.py +15 -34
  67. pdd/operation_log.py +342 -0
  68. pdd/path_resolution.py +140 -0
  69. pdd/postprocess.py +122 -97
  70. pdd/preprocess.py +68 -12
  71. pdd/preprocess_main.py +33 -1
  72. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  73. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  74. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  75. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  76. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  77. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  78. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  79. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  80. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  81. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  82. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  83. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  84. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
  85. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  86. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  87. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  88. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  89. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  90. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  91. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  92. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  93. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  94. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  95. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  96. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  97. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  98. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  99. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  100. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  101. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  102. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  103. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  104. pdd/prompts/agentic_update_LLM.prompt +192 -338
  105. pdd/prompts/auto_include_LLM.prompt +22 -0
  106. pdd/prompts/change_LLM.prompt +3093 -1
  107. pdd/prompts/detect_change_LLM.prompt +571 -14
  108. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  109. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  110. pdd/prompts/generate_test_LLM.prompt +19 -1
  111. pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
  112. pdd/prompts/insert_includes_LLM.prompt +262 -252
  113. pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
  114. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  115. pdd/remote_session.py +876 -0
  116. pdd/server/__init__.py +52 -0
  117. pdd/server/app.py +335 -0
  118. pdd/server/click_executor.py +587 -0
  119. pdd/server/executor.py +338 -0
  120. pdd/server/jobs.py +661 -0
  121. pdd/server/models.py +241 -0
  122. pdd/server/routes/__init__.py +31 -0
  123. pdd/server/routes/architecture.py +451 -0
  124. pdd/server/routes/auth.py +364 -0
  125. pdd/server/routes/commands.py +929 -0
  126. pdd/server/routes/config.py +42 -0
  127. pdd/server/routes/files.py +603 -0
  128. pdd/server/routes/prompts.py +1347 -0
  129. pdd/server/routes/websocket.py +473 -0
  130. pdd/server/security.py +243 -0
  131. pdd/server/terminal_spawner.py +217 -0
  132. pdd/server/token_counter.py +222 -0
  133. pdd/summarize_directory.py +236 -237
  134. pdd/sync_animation.py +8 -4
  135. pdd/sync_determine_operation.py +329 -47
  136. pdd/sync_main.py +272 -28
  137. pdd/sync_orchestration.py +289 -211
  138. pdd/sync_order.py +304 -0
  139. pdd/template_expander.py +161 -0
  140. pdd/templates/architecture/architecture_json.prompt +41 -46
  141. pdd/trace.py +1 -1
  142. pdd/track_cost.py +0 -13
  143. pdd/unfinished_prompt.py +2 -1
  144. pdd/update_main.py +68 -26
  145. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
  146. pdd_cli-0.0.121.dist-info/RECORD +229 -0
  147. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  148. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
  149. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
  150. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
  151. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
@@ -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)