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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -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 +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- 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 +27 -3
- pdd/core/cloud.py +237 -0
- 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 +204 -4
- 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-DQ3wkeQ2.js +449 -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 +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- 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 +131 -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 +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -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 +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -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 +136 -75
- 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 +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {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(
|
|
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.
|
|
116
|
-
2.
|
|
117
|
-
3.
|
|
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(
|
|
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
|
|
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)
|