diff --git a/pdd/commands/modify.py b/pdd/commands/modify.py index 48cefa2a0..3ce812a93 100644 --- a/pdd/commands/modify.py +++ b/pdd/commands/modify.py @@ -189,29 +189,51 @@ def change( @click.command() @click.argument("files", nargs=-1) +@click.option( + "--all", + "all_", + is_flag=True, + default=False, + help="Repository-wide update (same as passing no file arguments).", +) @click.option("--extensions", help="Comma-separated extensions for repo mode.") @click.option("--directory", help="Directory to scan for repo mode.") @click.option("--git", is_flag=True, help="Use git history for original code.") @click.option("--output", help="Output path for the updated prompt.") @click.option("--simple", is_flag=True, default=False, help="Use legacy simple update.") @click.option("--base-branch", type=str, default="main", help="Base branch for change detection in repo mode (default: main).") +@click.option( + "--budget", + type=float, + default=None, + help="Repository-wide only: stop processing once total update cost reaches this cap.", +) +@click.option( + "--dry-run", + is_flag=True, + default=False, + help="Repository-wide only: show which prompts would be updated without calling the LLM or writing outputs.", +) @click.pass_context @log_operation(operation="update", clears_run_report=True) @track_cost def update( ctx: click.Context, files: Tuple[str, ...], + all_: bool, extensions: Optional[str], directory: Optional[str], git: bool, output: Optional[str], simple: bool, base_branch: str, + budget: Optional[float], + dry_run: bool, ) -> Optional[Tuple[Any, float, str]]: """ Update the original prompt file based on code changes. - Repo-wide mode (no args): Scan entire repo. + Repo-wide mode (no args, or --all): Scan entire repo. Single-file mode (1 arg): Update prompt for specific code file. """ ctx.ensure_object(dict) @@ -227,10 +249,16 @@ def update( ) if len(files) > 3: raise click.UsageError("Too many arguments. Max 3: ") + if all_ and len(files) > 0: + raise click.UsageError( + "Cannot combine --all with file paths; use repository-wide mode with no arguments or only --all." + ) + if budget is not None and budget <= 0: + raise click.UsageError("--budget must be a positive number") try: # Handle argument counts per modify_python.prompt spec (aligned with README) - if len(files) == 0: + if len(files) == 0 or all_: # Repo-wide mode is_repo_mode = True input_prompt_file = None @@ -280,6 +308,14 @@ def update( raise click.UsageError( "--base-branch can only be used in repository-wide mode" ) + if dry_run: + raise click.UsageError( + "--dry-run is only valid in repository-wide mode (no file arguments, or use --all)." + ) + if budget is not None: + raise click.UsageError( + "--budget is only valid in repository-wide mode (no file arguments, or use --all)." + ) # Call update_main with correct parameters ret = update_main( @@ -294,6 +330,8 @@ def update( directory=directory, simple=simple, base_branch=base_branch, + budget=budget, + dry_run=dry_run, ) if ret is None: diff --git a/pdd/core/cli.py b/pdd/core/cli.py index 12783a721..891ef644d 100644 --- a/pdd/core/cli.py +++ b/pdd/core/cli.py @@ -25,9 +25,12 @@ def _strip_ansi_codes(text: str) -> str: """Remove ANSI escape codes from text for clean log output.""" - # Pattern matches ANSI escape sequences - ansi_escape = re.compile(r'\x1b\[[0-9;]*m') - return ansi_escape.sub('', text) + # Covers common CSI sequences (\x1b[...m, \x1b[...K, cursor moves), + # plus OSC sequences (\x1b]...BEL or \x1b]...\x1b\\) used by some terminals. + csi = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") + osc = re.compile(r"\x1b\].*?(?:\x07|\x1b\\)") + text = osc.sub("", text) + return csi.sub("", text) class OutputCapture: diff --git a/pdd/core/dump.py b/pdd/core/dump.py index 356d3cf5d..4128de74f 100644 --- a/pdd/core/dump.py +++ b/pdd/core/dump.py @@ -21,6 +21,51 @@ from .errors import console, get_core_dump_errors +def _extract_sync_steps_from_file_contents(file_contents: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Build per-operation sync step records from any attached *_sync.log files. + + The sync log is JSONL. We only convert "operation" entries (not "event" entries) + into a simplified list suitable for core dump inspection. + """ + steps: List[Dict[str, Any]] = [] + for path_key, content in (file_contents or {}).items(): + if not isinstance(path_key, str) or not path_key.endswith("_sync.log"): + continue + if not isinstance(content, str) or content.startswith("<"): + continue + for line in content.splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except Exception: + continue + # Skip events; keep operation rows + if entry.get("type") == "event": + continue + op = entry.get("operation") + if not op: + continue + details = entry.get("details") or {} + steps.append( + { + "operation": op, + "success": bool(entry.get("success")), + "cost": entry.get("actual_cost"), + "model": entry.get("model") or "unknown", + "duration": entry.get("duration"), + "reason": entry.get("reason"), + "error": entry.get("error"), + "failure_summary": entry.get("error") or (details.get("failure_reason") if isinstance(details, dict) else None), + "test_output_excerpt": details.get("test_output_excerpt") if isinstance(details, dict) else None, + "source_log": path_key, + } + ) + return steps + + def garbage_collect_core_dumps(keep: int = 10) -> int: """Delete old core dumps, keeping only the most recent `keep` files. @@ -77,13 +122,13 @@ def _write_core_dump( dump_path = core_dump_dir / f"pdd-core-{timestamp}.json" steps: List[Dict[str, Any]] = [] - for i, result_tuple in enumerate(normalized_results): - command_name = ( - invoked_subcommands[i] if i < len(invoked_subcommands) else f"Unknown Command {i+1}" - ) + step_count = max(len(invoked_subcommands), len(normalized_results)) + for i in range(step_count): + command_name = invoked_subcommands[i] if i < len(invoked_subcommands) else f"Unknown Command {i+1}" + result_tuple = normalized_results[i] if i < len(normalized_results) else None - cost = None - model_name = None + cost: Optional[float] = None + model_name: Optional[str] = None if isinstance(result_tuple, tuple) and len(result_tuple) == 3: _result_data, cost, model_name = result_tuple @@ -92,7 +137,7 @@ def _write_core_dump( "step": i + 1, "command": command_name, "cost": cost, - "model": model_name, + "model": (model_name or "unknown"), } ) @@ -128,6 +173,12 @@ def _write_core_dump( meta_file.stem.endswith(f"_{c}") for c in ["generate", "test", "run", "fix", "update"] ): core_dump_files.add(str(meta_file.resolve())) + # Include operation logs and run reports (critical for sync debugging) + # These are line-delimited JSON logs and are safe to attach (size-gated below). + for log_file in meta_dir.glob("*_sync.log"): + core_dump_files.add(str(log_file.resolve())) + for run_file in meta_dir.glob("*_run.json"): + core_dump_files.add(str(run_file.resolve())) # Auto-include PDD config files if they exist config_files = [ @@ -174,7 +225,7 @@ def _write_core_dump( console.print(f"[warning]Debug snapshot: Error reading {file_path}: {e}[/warning]") payload: Dict[str, Any] = { - "schema_version": 1, + "schema_version": 2, "pdd_version": __version__, "timestamp_utc": timestamp, "argv": sys.argv[1:], # without the 'pdd' binary name @@ -205,6 +256,9 @@ def _write_core_dump( "file_contents": file_contents, "terminal_output": terminal_output, } + sync_steps = _extract_sync_steps_from_file_contents(file_contents) + if sync_steps: + payload["sync_steps"] = sync_steps if exit_reason is not None: payload["exit_reason"] = exit_reason diff --git a/pdd/core/errors.py b/pdd/core/errors.py index 05c10e16d..a755ecb09 100644 --- a/pdd/core/errors.py +++ b/pdd/core/errors.py @@ -3,7 +3,7 @@ """ import os import traceback -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import click from rich.console import Console from rich.markup import MarkupError, escape @@ -41,6 +41,32 @@ def clear_core_dump_errors() -> None: _core_dump_errors.clear() +def record_core_dump_error( + *, + command: str, + type: str, + message: str, + details: Optional[Dict[str, Any]] = None, + traceback_text: Optional[str] = None, +) -> None: + """Record a structured error entry for core dumps. + + Use this for non-exception "logical failures" (budget exhaustion, retry limits, + cycle detection, etc.) so core dumps contain actionable context even when the + CLI run terminates without raising an exception. + """ + error_record: Dict[str, Any] = { + "command": command, + "type": type, + "message": message, + } + if details: + error_record["details"] = details + if traceback_text: + error_record["traceback"] = traceback_text + _core_dump_errors.append(error_record) + + def _format_interrupt_reason(ctx: Dict[str, Any]) -> str: """Build a human-readable reason from agentic interrupt context.""" step = ctx.get("current_step") diff --git a/pdd/core/llm_trace.py b/pdd/core/llm_trace.py new file mode 100644 index 000000000..5c8b89146 --- /dev/null +++ b/pdd/core/llm_trace.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import json +import re +from contextlib import contextmanager +from contextvars import ContextVar +from dataclasses import dataclass, asdict +from typing import Any, Dict, Iterator, Optional + + +@dataclass +class LLMTracePair: + prompt: str + response: str + model: str = "unknown" + + +_current_operation: ContextVar[Optional[str]] = ContextVar("pdd_current_operation", default=None) +_last_pair_by_operation: ContextVar[Dict[str, LLMTracePair]] = ContextVar( + "pdd_llm_last_pair_by_operation", default={} +) + + +_SENSITIVE_PATTERNS = [ + # Common bearer tokens / API keys + re.compile(r"(?i)\b(bearer)\s+[a-z0-9\-_\.=:+/]{10,}"), + re.compile(r"(?i)\b(api[_-]?key|token|secret|password)\b\s*[:=]\s*[^\s\"']{6,}"), +] + + +def set_current_operation(operation: Optional[str]) -> None: + _current_operation.set(operation) + + +@contextmanager +def operation_scope(operation: str) -> Iterator[None]: + token = _current_operation.set(operation) + try: + yield + finally: + _current_operation.reset(token) + + +def _truncate(text: str, limit_chars: int) -> str: + if text is None: + return "" + if len(text) <= limit_chars: + return text + return text[:limit_chars] + f"\n... (truncated, {len(text)} total chars)" + + +def _redact(text: str) -> str: + if not text: + return text + redacted = text + for pat in _SENSITIVE_PATTERNS: + redacted = pat.sub("", redacted) + return redacted + + +def record_llm_pair( + *, + prompt: Any, + response: Any, + model: str = "unknown", + prompt_limit_chars: int = 20_000, + response_limit_chars: int = 20_000, +) -> None: + """ + Record the most recent (prompt, raw_response) pair for the current operation. + Intended to be called by llm_invoke. + """ + op = _current_operation.get() + if not op: + return + + try: + prompt_text = prompt if isinstance(prompt, str) else json.dumps(prompt, ensure_ascii=False, default=str) + except Exception: + prompt_text = str(prompt) + + try: + response_text = response if isinstance(response, str) else json.dumps(response, ensure_ascii=False, default=str) + except Exception: + response_text = str(response) + + pair = LLMTracePair( + prompt=_truncate(_redact(prompt_text), prompt_limit_chars), + response=_truncate(_redact(response_text), response_limit_chars), + model=str(model or "unknown"), + ) + + current = dict(_last_pair_by_operation.get() or {}) + current[op] = pair + _last_pair_by_operation.set(current) + + +def pop_last_pair(operation: str) -> Optional[Dict[str, Any]]: + """Pop and return the last recorded pair for an operation (as a dict).""" + current = dict(_last_pair_by_operation.get() or {}) + pair = current.pop(operation, None) + _last_pair_by_operation.set(current) + return asdict(pair) if pair else None + diff --git a/pdd/llm_invoke.py b/pdd/llm_invoke.py index 002b42e7d..57aa9afa3 100644 --- a/pdd/llm_invoke.py +++ b/pdd/llm_invoke.py @@ -12,6 +12,12 @@ # --- Configure Standard Python Logging --- logger = logging.getLogger("pdd.llm_invoke") +# Optional lightweight trace for core dumps (best-effort). +try: + from .core.llm_trace import record_llm_pair as _record_llm_pair +except Exception: # pragma: no cover + _record_llm_pair = None # type: ignore + # Environment variable to control log level PDD_LOG_LEVEL = os.getenv("PDD_LOG_LEVEL", "INFO") PRODUCTION_MODE = os.getenv("PDD_ENVIRONMENT") == "production" @@ -412,6 +418,20 @@ def _llm_invoke_cloud( if response.status_code == 200: data = response.json() result = data.get("result") + # Best-effort trace: store the final prompt/messages and raw cloud "result". + if _record_llm_pair is not None: + try: + trace_prompt = payload.get("messages") if payload.get("messages") is not None else { + "prompt": payload.get("prompt"), + "inputJson": payload.get("inputJson"), + } + _record_llm_pair( + prompt=trace_prompt, + response=result, + model=str(data.get("modelName", "cloud_model")), + ) + except Exception: + pass # Validate with Pydantic if specified if output_pydantic and result: @@ -1891,6 +1911,18 @@ def llm_invoke( else: raise ValueError("Either 'messages' or both 'prompt' and 'input_json' must be provided.") + # Best-effort: precompute a compact representation of the final messages. + # We'll record (messages, raw_response) after we receive the response. + trace_prompt_repr: Any = None + try: + if not use_batch_mode: + trace_prompt_repr = formatted_messages + else: + # Avoid huge traces for batch requests. + trace_prompt_repr = None + except Exception: + trace_prompt_repr = None + # Handle None time (means "no reasoning requested") if time is None: time = 0.0 @@ -2563,6 +2595,16 @@ def calc_strength(candidate): # Result (String or Pydantic) try: raw_result = resp_item.choices[0].message.content + # Record the last (prompt, raw response) pair for the current operation. + if _record_llm_pair is not None and trace_prompt_repr is not None: + try: + _record_llm_pair( + prompt=trace_prompt_repr, + response=raw_result, + model=str(model_name_litellm), + ) + except Exception: + pass # Check if raw_result is None (likely cached corrupted data) if raw_result is None: @@ -2598,6 +2640,15 @@ def calc_strength(candidate): _LAST_CALLBACK_DATA["output_tokens"] = _LAST_CALLBACK_DATA.get("output_tokens", 0) + _accumulated_output_tokens # Extract result from retry retry_raw_result = retry_response.choices[0].message.content + if _record_llm_pair is not None and trace_prompt_repr is not None: + try: + _record_llm_pair( + prompt=trace_prompt_repr, + response=retry_raw_result, + model=str(model_name_litellm), + ) + except Exception: + pass if retry_raw_result is not None: logger.info(f"[SUCCESS] Cache bypass retry succeeded for item {i}") raw_result = retry_raw_result diff --git a/pdd/prompts/agentic_bug_orchestrator_python.prompt b/pdd/prompts/agentic_bug_orchestrator_python.prompt new file mode 100644 index 000000000..1f64df0a3 --- /dev/null +++ b/pdd/prompts/agentic_bug_orchestrator_python.prompt @@ -0,0 +1,176 @@ +context/python_preamble.prompt + +% Goal +Write the `pdd/agentic_bug_orchestrator.py` module. + +% Role & Scope +Orchestrator for the 12-step agentic bug investigation workflow. Runs each step as a separate agentic task, accumulates context between steps, and tracks overall progress and cost. + +% Per-Step Timeouts +The prompt spec must match the code constant exactly: +```python +BUG_STEP_TIMEOUTS: Dict[int, float] = { + 1: 240.0, # Duplicate Check + 2: 400.0, # Docs Check + 3: 400.0, # Triage + 4: 400.0, # API Research + 5: 600.0, # Reproduce (Complex) + 6: 600.0, # Root Cause (Complex) + 7: 600.0, # Prompt Classification (may auto-fix prompts) + 8: 340.0, # Test Plan + 9: 1000.0, # Generate Unit Test (Most Complex) + 10: 600.0, # Verify Unit Test + 11: 2000.0, # E2E Test (Complex - needs to discover env & run tests) + 12: 240.0, # Create PR +} +``` + +% Requirements +1. Function: `run_agentic_bug_orchestrator(issue_url: str, issue_content: str, repo_owner: str, repo_name: str, issue_number: int, issue_author: str, issue_title: str, *, cwd: Path, verbose: bool = False, quiet: bool = False) -> Tuple[bool, str, float, str, List[str]]` +2. Return 5-tuple: (success, final_message, total_cost, model_used, changed_files) +3. Run 10 steps sequentially, each as a separate `run_agentic_task()` call +4. Accumulate step outputs to pass as context to subsequent steps +5. Track total cost across all steps +6. For Step 7: Parse agent output for `FILES_CREATED: path1, path2` or `FILES_MODIFIED: path1, path2` lines to extract changed files (used for hard stop check and final summary) +7. For Step 9: Parse agent output for `E2E_FILES_CREATED: path1, path2` lines to extract E2E test files, extend changed_files list +8. Pass extracted files to Step 9 (E2E) and Step 10 (PR) via `files_to_stage` context variable for explicit git staging + +% Step Execution +For each step (1-10): +1. Load the step prompt template via `load_prompt_template(f"agentic_bug_step{n}_{name}_LLM")` +2. Format template with: issue_url, repo_owner, repo_name, issue_number, issue_content, issue_author, step1_output, step2_output, etc. +3. Call `run_agentic_task(formatted_prompt, cwd, ..., timeout=STEP_TIMEOUTS.get(step_num))` + - Import `STEP_TIMEOUTS` from `agentic_common` (Issue #261) + - Pass step-specific timeout to ensure complex steps get adequate time +4. Store the output for use by subsequent steps +5. Accumulate cost: `total_cost += step_cost` + +% Step Sequence +| Step | Template Name | Purpose | +|------|---------------|---------| +| 1 | agentic_bug_step1_duplicate_LLM | Search for duplicate issues | +| 2 | agentic_bug_step2_docs_LLM | Check documentation for user error | +| 3 | agentic_bug_step3_triage_LLM | Assess if enough info to proceed | +| 4 | agentic_bug_step4_reproduce_LLM | Attempt to reproduce the bug | +| 5 | agentic_bug_step5_root_cause_LLM | Analyze root cause | +| 6 | agentic_bug_step6_test_plan_LLM | Design test strategy | +| 7 | agentic_bug_step7_generate_LLM | Generate failing unit test | +| 8 | agentic_bug_step8_verify_LLM | Verify test catches the bug | +| 9 | agentic_bug_step9_e2e_test_LLM | Generate and run E2E tests | +| 10 | agentic_bug_step10_pr_LLM | Create draft PR and link to issue | + +% Context Accumulation +- Step 1 receives: issue_content +- Step 2 receives: issue_content, step1_output +- Step 3 receives: issue_content, step1_output, step2_output +- Step 4 receives: issue_content, step1_output through step3_output +- ... and so on +- Step 8 receives: issue_content, step1_output through step7_output +- Step 9 receives: issue_content, step1_output through step8_output, worktree_path, files_to_stage +- Step 10 receives: issue_content, step1_output through step9_output, worktree_path, files_to_stage + +% Files to Stage +After Step 7 parses FILES_CREATED/FILES_MODIFIED, pass the extracted file list to Steps 9 and 10: +- `context["files_to_stage"] = ", ".join(changed_files)` +- After Step 9 parses E2E_FILES_CREATED, extend changed_files and update files_to_stage +- This ensures Step 9 (E2E) and Step 10 (PR) know exactly which files to include +- Step 10 prompt uses `{files_to_stage}` to display the explicit list of files to stage + +% Early Exit Conditions (Hard Stops) + +The orchestrator must parse step output to detect stop conditions: + +| Step | Stop Condition | Detection | +|------|----------------|-----------| +| 1 | Issue is a duplicate | Output contains "Duplicate of #" | +| 2 | "Feature Request" or "User Error" | Output contains "Feature Request (Not a Bug)" or "User Error (Not a Bug)" | +| 3 | Needs more info from author | Output contains "Needs More Info" | +| 7 | No test file generated | No FILES_CREATED or FILES_MODIFIED line in output, or empty file list | +| 8 | Test doesn't fail correctly | Output contains "FAIL: Test does not work as expected" | +| 9 | E2E test doesn't catch bug | Output contains "E2E_FAIL: Test does not catch bug correctly" | + +**Hard stop behavior:** +- Return `(False, "Stopped at step N: {reason}", total_cost, model_used, changed_files)` +- The step has already posted its findings to GitHub before returning +- Do NOT continue to subsequent steps + +**Soft failures (continue):** +- If `run_agentic_task()` returns `(False, ...)` but does NOT match a hard stop condition: log warning and continue +- This applies to all steps - only the specific hard stop patterns above terminate the workflow +- Steps should post their findings to GitHub even on partial failure + +% Git Worktree Isolation + +Before Step 7, create an isolated git worktree for test generation and PR creation. This prevents the workflow from disturbing the user's current branch. + +1. Helper function: `_setup_worktree(cwd: Path, issue_number: int, quiet: bool) -> Tuple[Optional[Path], Optional[str]]` + - Create worktree at `.pdd/worktrees/fix-issue-{issue_number}/` relative to git root + - Branch name: `fix/issue-{issue_number}` + - If worktree already exists at path, remove with `git worktree remove --force` + - If directory exists but is NOT a worktree, remove with `shutil.rmtree()` + - If branch already exists locally, delete with `git branch -D` + - Create worktree: `git worktree add -b {branch} {path} HEAD` + - Return (worktree_path, None) on success, (None, error_msg) on failure + +2. In orchestrator loop, before Step 7: + - Call `_setup_worktree(cwd, issue_number, quiet)` + - If worktree creation fails, return early with error: `(False, "Failed to create worktree: {error}", ...)` + - Switch working directory to worktree path for Steps 7-10 + - Add `worktree_path` to context dict (for Step 10 prompt formatting) + - Print: `"[blue]Working in worktree: {worktree_path}[/blue]"` (unless quiet) + +3. Additional helper functions: + - `_get_git_root(cwd: Path) -> Optional[Path]` - Get repo root via `git rev-parse --show-toplevel` + - `_worktree_exists(cwd: Path, worktree_path: Path) -> bool` - Check if path is in `git worktree list --porcelain` output + - `_branch_exists(cwd: Path, branch: str) -> bool` - Check via `git show-ref --verify refs/heads/{branch}` + - `_remove_worktree(cwd: Path, worktree_path: Path) -> Tuple[bool, str]` - Remove via `git worktree remove --force` + - `_delete_branch(cwd: Path, branch: str) -> Tuple[bool, str]` - Delete via `git branch -D` + +% Console Output + +The orchestrator must provide real-time feedback to the user via rich console output: + +1. **Header** (at start): + ``` + 🔍 Investigating issue #{issue_number}: "{issue_title}" + ``` + +2. **Step progress** (before each step): + ``` + [Step N/10] {step_description}... + ``` + +3. **Agentic output** (during each step): + - Stream or display the output from `run_agentic_task()` so the user can see what the agent is doing + - Use `verbose` flag to control detail level + +4. **Step result** (after each step): + ``` + → {brief_result} + ``` + Examples: "No duplicates found", "Confirmed bug", "Needs More Info (stopping)" + +5. **Hard stop message** (if workflow terminates early): + ``` + ⏹️ Investigation stopped at Step N: {reason} + ``` + +6. **Final summary** (at end): + ``` + ✅ Investigation complete (or ❌ Investigation failed) + Total cost: ${total_cost:.4f} + Files changed: {comma_separated_list} + Worktree: {worktree_path} (if worktree was created) + PR created: #{pr_number} (if applicable) + ``` + +Use `quiet` flag to suppress all output except errors. Use `verbose` flag to show full agentic task output. + +% Dependencies +Import from `agentic_common`: `run_agentic_task`, `STEP_TIMEOUTS` +context/agentic_common_example.py +context/load_prompt_template_example.py +context/agentic_bug_example.py + +% Deliverables +- Code: `pdd/agentic_bug_orchestrator.py` \ No newline at end of file diff --git a/pdd/prompts/agentic_bug_step11_e2e_test_LLM.prompt b/pdd/prompts/agentic_bug_step11_e2e_test_LLM.prompt index aa9ce7306..4229a9512 100644 --- a/pdd/prompts/agentic_bug_step11_e2e_test_LLM.prompt +++ b/pdd/prompts/agentic_bug_step11_e2e_test_LLM.prompt @@ -98,11 +98,15 @@ When using `page.route()` or similar API mocking in E2E tests: - If the last handler calls `route.continue()`, the previous handler for the same URL is skipped entirely - ALWAYS use ONE `page.route()` call per URL pattern that switches on request method: ```typescript - await page.route('**/api/endpoint', async (route) => { + await page.route("**/api/boards/**", async (route) => { const method = route.request().method(); - if (method === 'GET') { /* handle GET */ } - else if (method === 'POST') { /* handle POST */ } - else { await route.continue(); } + if (method === 'GET') { + return route.fulfill({ status: 200, body: JSON.stringify({ boards: [] }) }); + } + if (method === 'POST') { + return route.fulfill({ status: 500, body: JSON.stringify({ message: "Save failed (500)" }) }); + } + return route.continue(); }); ``` - NEVER register two `page.route()` calls for the same URL pattern — the second silently overrides the first diff --git a/pdd/prompts/agentic_e2e_fix_orchestrator_python.prompt b/pdd/prompts/agentic_e2e_fix_orchestrator_python.prompt new file mode 100644 index 000000000..3f5468d00 --- /dev/null +++ b/pdd/prompts/agentic_e2e_fix_orchestrator_python.prompt @@ -0,0 +1,34 @@ +context/python_preamble.prompt + +% Goal +Design the `agentic_e2e_fix_orchestrator` for the 9‑step E2E‑fix workflow. + +% Loop Control & Status Tokens +The orchestrator must interpret special status markers in step outputs: +- ALL_TESTS_PASS +- CONTINUE_CYCLE +- NOT_A_BUG + +The orchestrator must treat NOT_A_BUG as a first‑class loop‑control token, alongside ALL_TESTS_PASS and CONTINUE_CYCLE. + +% Environment Safety: _check_e2e_environment +Before running any E2E workflow, the orchestrator must explicitly call the `_check_e2e_environment` helper function. +This verifies that E2E tests can safely run in the current environment (CI or local), including: +- required tools (Python, Node, browsers, etc.) +- required environment variables and endpoints +If the environment is unsafe, the orchestrator stops early with an informative message and does not run real E2E tests. + +% Skipped Steps & Memory: skipped_steps_memory +The orchestrator tracks last_completed_step and, when resuming, may skip earlier steps. +However, it must still preserve and reload previous step outputs so that later steps receive a coherent context. +skipped_steps persists across cycles and is NOT cleared when step_outputs is cleared. +This behavior is called `skipped_steps_memory`: even skipped steps contribute their prior stepN_output values into downstream prompts. +The skipped_steps (or equivalent) state persists across cycles and is NOT cleared when step_outputs is cleared between cycles. + +% Convergence (Issue #903) +Empty dev-units short-circuit: when Step 5 yields no dev-units, the orchestrator must short-circuit the cycle (skip dependent work) instead of continuing as if work remained. + +Per-cycle file-hash comparison: compare file hashes each cycle so unchanged files are not mistaken for fresh changes; use per-cycle file-hash snapshots to decide whether dev-units or code actually moved. + +This file intentionally contains the literal tokens `_check_e2e_environment` and `skipped_steps_memory` +so tests can assert the orchestrator prompt documents these behaviors. diff --git a/pdd/prompts/core_dump_requirements_LLM.prompt b/pdd/prompts/core_dump_requirements_LLM.prompt new file mode 100644 index 000000000..ecdcf0866 --- /dev/null +++ b/pdd/prompts/core_dump_requirements_LLM.prompt @@ -0,0 +1,110 @@ +Defines durable debug snapshot (core dump) requirements for PDD CLI + sync. + + +{ + "type": "module", + "module": { + "functions": [ + {"name": "core_dump_requirements", "signature": "()", "returns": "Specification (text)"}, + {"name": "acceptance_criteria", "signature": "()", "returns": "List[str]"} + ] + } +} + + +% You are maintaining PDD’s debugging artifacts (core dumps / debug snapshots). +% This prompt is the authoritative specification. When regenerating or modifying code, +% implement and preserve the behaviors below exactly. + + +- Scope: PDD CLI debug snapshots written to `.pdd/core_dumps/pdd-core-*.json`. +- Related subsystems: CLI output capture, core dump writer, sync orchestration logging, and LLM invocation tracing. +- Goal: when a command fails, the snapshot must contain enough signal to diagnose without reproducing. + + + +1) **Schema versioning** + - Maintain a top-level integer `schema_version`. + +2) **Core dump payload must include** + - `argv`, `cwd`, `platform`, `pdd_version`, `timestamp_utc` + - `global_options` + - `invoked_subcommands` + - `total_cost` + - `steps` (per top-level subcommand execution step) + - `errors` (structured error entries) + - `file_contents` (tracked files and relevant `.pdd/meta` artifacts, size-capped) + - `terminal_output` (captured stdout/stderr, ANSI-stripped) + +3) **Top-level step records (`steps`)** + - `steps[*].model` must never be blank; if unknown use `"unknown"`. + - If `invoked_subcommands` has \(N\) commands but results are missing/partial, still emit \(N\) step entries with unknown cost/model as needed. + +4) **Terminal output capture** + - Capture combined stdout/stderr into `terminal_output`. + - Strip ANSI escapes before saving: + - CSI sequences (e.g. `\x1b[...m`, cursor moves, erase-in-line) + - OSC sequences (e.g. `\x1b]...BEL` or `\x1b]...\x1b\\`) + +5) **Attach sync operation artifacts** + - Auto-include into `file_contents` (subject to size caps): + - `.pdd/meta/*_sync.log` (JSONL) + - `.pdd/meta/*_run.json` (run report) + - Operation log entries must include: + - `operation`, `success`, `duration`, `actual_cost`, `model`, `error` + - `reason` and any relevant `details` + +6) **Expanded sync steps in the core dump** + - Build a top-level `sync_steps` array by parsing attached `*_sync.log` file contents. + - Include one `sync_steps[]` record per operation row (skip rows where `type == "event"`). + - Each `sync_steps[]` record must include: + - `operation`, `success`, `cost` (from operation log `actual_cost`), `model`, `duration` + - `failure_summary` (prefer operation log `error`, else `details.failure_reason` if present) + - `test_output_excerpt` (if present) + - `source_log` (the log file key in `file_contents`) + +7) **Capture test output excerpts (failed attempts)** + - When a test runner execution fails during a sync/fix attempt, capture combined stdout+stderr. + - Store a truncated excerpt on the operation log entry: + - `details.test_output_excerpt` + - Truncation: + - Target cap: ~5KB (5120 chars) + - Must include a clear truncation marker and original size (e.g. `... (truncated, N total chars)`). + +8) **Logical failures must be structured errors** + - If a sync/fix loop terminates due to non-exception conditions (e.g. max retries, budget exhaustion, cycle detection, consecutive-op breaker), record a structured entry in core dump `errors`. + - Each structured error must include: + - `command`, `type`, `message` + - `details` containing at least `basename`, `language` (when applicable), and relevant counters/state. + +9) **LLM request/response capture (moderate, failure-only)** + - Capture the final prompt/messages sent and the raw LLM response for each failed operation. + - Guardrails: + - Only keep the *last* request/response pair per operation. + - Only persist when the operation fails. + - Truncate prompt and response to <= 20,000 chars each. + - Redact obvious secrets/tokens. + - Store on the operation log entry: + - `details.llm_trace = { prompt, response, model }` + +10) **Safety and size caps** + - `file_contents` must cap individual file sizes at 50KB and mark oversized/binary files explicitly (e.g. ``, ``). + - Core dump creation must never crash the CLI (best-effort; errors in dumping are warnings only). + + + +Avoid non-deterministic directives (``, ``) in this spec. Keep requirements self-contained and deterministic. + + + +The following checks must pass in the codebase after regeneration: +- Core dump JSON has `schema_version` and required top-level fields. +- `steps[*].model` is never blank (defaults to `"unknown"`). +- `terminal_output` contains no ANSI sequences (CSI/OSC). +- `file_contents` includes `*_sync.log` and `*_run.json` when present. +- `sync_steps` is populated from `*_sync.log` operation rows and includes `source_log`. +- Failed sync/fix terminations without exceptions produce structured `errors[]` entries. +- Failed operations attach `details.llm_trace` with truncated/redacted prompt+response. +- Failed test runs attach `details.test_output_excerpt` truncated to ~5KB with a truncation marker. + + diff --git a/pdd/prompts/core_dump_smoke_python.prompt b/pdd/prompts/core_dump_smoke_python.prompt new file mode 100644 index 000000000..fc654c884 --- /dev/null +++ b/pdd/prompts/core_dump_smoke_python.prompt @@ -0,0 +1 @@ +Write a Python function add(a, b) that returns a + b. diff --git a/pdd/sync_main.py b/pdd/sync_main.py index 512329c42..f3bb39013 100644 --- a/pdd/sync_main.py +++ b/pdd/sync_main.py @@ -28,6 +28,7 @@ from .sync_orchestration import sync_orchestration from .sync_tui import DEFAULT_STEER_TIMEOUT_S from .template_expander import expand_template +from .core.errors import record_core_dump_error # Blocklist of characters that are dangerous in shell contexts or malformed as paths. # Everything else is allowed — frameworks use [], (), +, @, dots, unicode, etc. @@ -717,6 +718,17 @@ def sync_main( rprint(f"[yellow]Budget exhausted. Skipping sync for '{lang}'.[/yellow]") overall_success = False aggregated_results["results_by_language"][lang] = {"success": False, "error": "Budget exhausted"} + record_core_dump_error( + command="sync", + type="BudgetExhausted", + message="Budget exhausted. Skipping remaining languages.", + details={ + "basename": basename, + "language": lang, + "remaining_budget": remaining_budget, + "total_cost_so_far": total_cost, + }, + ) continue try: diff --git a/pdd/sync_orchestration.py b/pdd/sync_orchestration.py index 454962a98..8561f1bf4 100644 --- a/pdd/sync_orchestration.py +++ b/pdd/sync_orchestration.py @@ -66,6 +66,21 @@ from .get_run_command import get_run_command_for_file from .pytest_output import extract_failing_files_from_output, _find_project_root from . import DEFAULT_STRENGTH +from .core.errors import record_core_dump_error +from .core.llm_trace import set_current_operation, pop_last_pair + + +def _truncate_text(text: str, limit_chars: int) -> str: + if text is None: + return "" + if len(text) <= limit_chars: + return text + return text[:limit_chars] + f"\n... (truncated, {len(text)} total chars)" + + +def _run_fix_operation_test_subprocess(*args: Any, **kwargs: Any) -> Any: + """subprocess.run for fix-phase test capture; separate symbol so tests can patch reliably.""" + return subprocess.run(*args, **kwargs) # --- Helper Functions --- @@ -1684,7 +1699,7 @@ def sync_worker_logic(): skipped_operations: List[str] = [] errors: List[str] = [] start_time = time.time() - last_model_name: str = "" + last_model_name: str = "unknown" operation_history: List[str] = [] MAX_CYCLE_REPEATS = 2 try: @@ -1829,7 +1844,22 @@ def sync_worker_logic(): else: break if consecutive_fixes >= 5: - errors.append(f"Detected {consecutive_fixes} consecutive fix operations. Breaking infinite fix loop.") + msg = f"Detected {consecutive_fixes} consecutive fix operations. Breaking infinite fix loop." + errors.append(msg) + record_core_dump_error( + command="sync", + type="LogicalFailure", + message=msg, + details={ + "basename": basename, + "language": language, + "reason": "consecutive_fix_limit", + "consecutive": consecutive_fixes, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + }, + ) break if operation == 'test': @@ -1840,7 +1870,22 @@ def sync_worker_logic(): else: break if consecutive_tests >= MAX_CONSECUTIVE_TESTS: - errors.append(f"Detected {consecutive_tests} consecutive test operations. Breaking infinite test loop.") + msg = f"Detected {consecutive_tests} consecutive test operations. Breaking infinite test loop." + errors.append(msg) + record_core_dump_error( + command="sync", + type="LogicalFailure", + message=msg, + details={ + "basename": basename, + "language": language, + "reason": "consecutive_test_limit", + "consecutive": consecutive_tests, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + }, + ) break # Bug #157 fix: Prevent infinite crash retry loops @@ -1852,7 +1897,22 @@ def sync_worker_logic(): else: break if consecutive_crashes >= MAX_CONSECUTIVE_CRASHES: - errors.append(f"Detected {consecutive_crashes} consecutive crash operations. Breaking infinite crash loop.") + msg = f"Detected {consecutive_crashes} consecutive crash operations. Breaking infinite crash loop." + errors.append(msg) + record_core_dump_error( + command="sync", + type="LogicalFailure", + message=msg, + details={ + "basename": basename, + "language": language, + "reason": "consecutive_crash_limit", + "consecutive": consecutive_crashes, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + }, + ) break if operation == 'test_extend': @@ -1901,10 +1961,40 @@ def sync_worker_logic(): success = operation in ['all_synced', 'nothing'] error_msg = None if operation == 'fail_and_request_manual_merge': - errors.append(f"Manual merge required: {decision.reason}") + msg = f"Manual merge required: {decision.reason}" + errors.append(msg) + record_core_dump_error( + command="sync", + type="ManualMergeRequired", + message=msg, + details={ + "basename": basename, + "language": language, + "operation": operation, + "reason": decision.reason, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + }, + ) error_msg = decision.reason elif operation == 'error': - errors.append(f"Error determining operation: {decision.reason}") + msg = f"Error determining operation: {decision.reason}" + errors.append(msg) + record_core_dump_error( + command="sync", + type="DecisionError", + message=msg, + details={ + "basename": basename, + "language": language, + "operation": operation, + "reason": decision.reason, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + }, + ) error_msg = decision.reason update_log_entry(log_entry, success=success, cost=0.0, model='none', duration=0.0, error=error_msg) @@ -1967,8 +2057,13 @@ def sync_worker_logic(): success = False op_start_time = time.time() include_deps_override = None # Issue #522: Captured before auto-deps strips tags + test_output_excerpt: Optional[str] = None # Issue #159 fix: Use atomic state for consistent run_report + fingerprint writes + set_current_operation(operation) + # Drop any stale LLM trace for this operation key so failure paths only + # attach pairs from the current attempt (success paths do not pop). + pop_last_pair(operation) with AtomicStateUpdate(basename, language) as atomic_state: # --- Execute Operation --- @@ -2305,23 +2400,36 @@ def __init__(self, rc, out, err): pytest_args.extend([f'--rootdir={project_root}', '-c', '/dev/null']) subprocess_kwargs['cwd'] = str(project_root) - test_result = subprocess.run(pytest_args, **subprocess_kwargs) + test_result = _run_fix_operation_test_subprocess(pytest_args, **subprocess_kwargs) else: - fix_cwd = str(test_cmd.cwd) if test_cmd.cwd is not None else str(pdd_files['test'].parent) - test_result = subprocess.run( + # Non-Python: shell command; honor TestCommand.cwd when set (monorepo configs). + fix_cwd = ( + str(test_cmd.cwd) + if test_cmd.cwd is not None + else str(Path(pdd_files["test"]).parent) + ) + test_result = _run_fix_operation_test_subprocess( test_cmd.command, shell=True, - capture_output=True, text=True, timeout=300, - stdin=subprocess.DEVNULL, env=clean_env, + capture_output=True, + text=True, + timeout=300, + stdin=subprocess.DEVNULL, + env=clean_env, cwd=fix_cwd, - start_new_session=True + start_new_session=True, ) error_content = f"Test output:\n{test_result.stdout}\n{test_result.stderr}" + if getattr(test_result, "returncode", 0) != 0: + # Capture for core dump / sync step records + test_output_excerpt = _truncate_text(error_content, 5 * 1024) else: # No test command available - trigger agentic fallback with context error_content = f"No test command available for {language}. Please run tests manually and provide error output." + test_output_excerpt = _truncate_text(error_content, 5 * 1024) except Exception as e: error_content = f"Test execution error: {e}" + test_output_excerpt = _truncate_text(error_content, 5 * 1024) error_file_path.write_text(error_content) # Bug #156 fix: Parse pytest output to find actual failing files @@ -2433,6 +2541,13 @@ def __init__(self, rc, out, err): _save_fingerprint_atomic(basename, language, operation, pdd_files, actual_cost, str(model_name), atomic_state=atomic_state, include_deps_override=include_deps_override) update_log_entry(log_entry, success=success, cost=actual_cost, model=model_name, duration=duration, error=errors[-1] if errors and not success else None) + if not success: + if test_output_excerpt: + log_entry.setdefault("details", {})["test_output_excerpt"] = test_output_excerpt + # Attach last LLM prompt/response pair (best-effort) for failed operations. + pair = pop_last_pair(operation) + if pair: + log_entry.setdefault("details", {})["llm_trace"] = pair append_log_entry(basename, language, log_entry) # Post-operation checks (simplified) @@ -2525,7 +2640,21 @@ def __init__(self, rc, out, err): if not success: if not errors: - errors.append(f"Operation '{operation}' failed.") + msg = f"Operation '{operation}' failed." + errors.append(msg) + record_core_dump_error( + command="sync", + type="OperationFailed", + message=msg, + details={ + "basename": basename, + "language": language, + "operation": operation, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + }, + ) break except BaseException as e: @@ -2539,6 +2668,21 @@ def __init__(self, rc, out, err): except: pass # Return result dict + if errors: + record_core_dump_error( + command="sync", + type="SyncFailed", + message="Sync terminated with errors (non-exception failure).", + details={ + "basename": basename, + "language": language, + "errors": errors, + "operations_completed": operations_completed, + "skipped_operations": skipped_operations, + "total_cost": current_cost_ref[0], + "model_name": last_model_name, + }, + ) return { 'success': not errors, 'operations_completed': operations_completed, diff --git a/pdd/update_main.py b/pdd/update_main.py index 19c258e23..5e6ffd2db 100644 --- a/pdd/update_main.py +++ b/pdd/update_main.py @@ -2,6 +2,7 @@ import re import subprocess import sys +from collections import Counter, defaultdict from typing import Tuple, Optional, List, Dict, Any, Set import click from rich import print as rprint @@ -25,7 +26,7 @@ from .git_update import git_update from .agentic_common import get_available_agents from .agentic_update import run_agentic_update -from .sync_determine_operation import calculate_sha256, read_fingerprint +from .sync_determine_operation import calculate_sha256, extract_include_deps, read_fingerprint from . import DEFAULT_TIME # Config/data files that should not get prompts in repo-scan mode. @@ -770,6 +771,115 @@ def update_file_pair(prompt_file: str, code_file: str, ctx: click.Context, repo: "error": str(e), } +def _read_text_byte_len(path: str) -> int: + """Best-effort size of file text for dry-run cost sizing (chars).""" + try: + return len(Path(path).read_text(encoding="utf-8", errors="ignore")) + except (OSError, IOError): + return 0 + + +def _repo_relative_md_dep(dep_str: str, repo_root_p: Path) -> Optional[str]: + """Return repo-relative path for a resolved include target, or None if outside repo / not doc.""" + dep_p = Path(dep_str) + if not dep_p.is_absolute(): + dep_p = (Path.cwd() / dep_p).resolve() + else: + dep_p = dep_p.resolve() + if dep_p.suffix.lower() not in (".md", ".mdx"): + return None + try: + rel = dep_p.relative_to(repo_root_p) + except ValueError: + return None + return str(rel).replace("\\", "/") + + +def _md_doc_prompt_reference_counts( + repo_root: str, prompt_paths: List[str] +) -> Dict[str, Set[str]]: + """Map repo-relative .md/.mdx → set of prompt paths that it (scan-wide).""" + repo_root_p = Path(repo_root).resolve() + doc_to_prompts: Dict[str, Set[str]] = defaultdict(set) + for prompt_path_str in prompt_paths: + deps = extract_include_deps(Path(prompt_path_str)) + for dep_str in deps: + rel = _repo_relative_md_dep(dep_str, repo_root_p) + if rel: + doc_to_prompts[rel].add(prompt_path_str) + return doc_to_prompts + + +def _included_docs_for_drift_report( + repo_root: str, + all_prompt_paths: List[str], + drifted_prompt_paths: List[str], +) -> List[Tuple[str, int]]: + """Docs referenced by at least one drifted prompt; count = prompts in full scan that include each doc.""" + drifted_set = set(drifted_prompt_paths) + doc_to_prompts = _md_doc_prompt_reference_counts(repo_root, all_prompt_paths) + rows: List[Tuple[str, int]] = [] + for doc_rel, prompters in doc_to_prompts.items(): + if not prompters & drifted_set: + continue + rows.append((doc_rel, len(prompters))) + return sorted(rows, key=lambda x: (-x[1], x[0])) + + +def _estimate_dry_run_cost_range( + ctx: click.Context, + repo_obj: git.Repo, + simple: bool, + changed_items: List[Tuple[str, str, str]], +) -> Tuple[float, float]: + """Flat heuristic total $ range for dry-run (not billed). + + Historically, each per-file update tends to cost around $0.50–$1.00. For dry-run, + report a simple flat estimate based on the number of drifted pairs. + """ + n = len(changed_items) + return (n * 0.5, n * 1.0) + + +def _print_repository_drift_report( + repo_root: str, + n_changed: int, + n_pairs: int, + changed_items: List[Tuple[str, str, str]], + included_docs: List[Tuple[str, int]], + cost_low: float, + cost_high: float, +) -> None: + """Print the repository drift summary for `pdd update --dry-run` (no LLM).""" + console.print() + console.print("[bold]Repository drift report:[/bold]") + console.print(f" Changed files: [cyan]{n_changed}[/cyan] of [cyan]{n_pairs}[/cyan] pairs") + console.print( + f" Estimated cost: [yellow]${cost_low:.2f}–${cost_high:.2f}[/yellow] " + "(flat $0.50–$1.00 per drifted pair; not billed in dry run)" + ) + console.print() + console.print(" [bold]Drifted modules:[/bold]") + rows = sorted(changed_items, key=lambda x: x[1]) + code_width = 0 + for _p, code_path, _r in rows: + code_width = max(code_width, len(os.path.relpath(code_path, repo_root))) + code_width = min(max(code_width, 12), 52) + for i, (prompt_path, code_path, _reason) in enumerate(rows, start=1): + cr = os.path.relpath(code_path, repo_root) + pr = os.path.relpath(prompt_path, repo_root) + pad = max(code_width - len(cr), 0) + console.print(f" {i}. {cr}{' ' * pad} → {pr}") + console.print() + console.print(" [bold]Included docs that may need updating:[/bold]") + if not included_docs: + console.print(" [dim](none detected among drifted prompts)[/dim]") + else: + for doc_rel, cnt in included_docs: + console.print(f" - {doc_rel} (included by {cnt} prompt{'s' if cnt != 1 else ''})") + console.print() + + def _find_prd_file(project_root: Path) -> Optional[Path]: """Find PRD file by convention: PRD.md, prd.md, *_prd.md, *_PRD.md.""" for pattern in ["PRD.md", "prd.md", "*_prd.md", "*_PRD.md"]: @@ -793,6 +903,8 @@ def update_main( temperature: Optional[float] = None, simple: bool = False, base_branch: str = "main", + budget: Optional[float] = None, + dry_run: bool = False, ) -> Optional[Tuple[str, float, str]]: """ CLI wrapper for updating prompts based on modified code. @@ -810,6 +922,8 @@ def update_main( :param strength: Optional strength parameter (overrides ctx.obj if provided). :param temperature: Optional temperature parameter (overrides ctx.obj if provided). :param base_branch: Git branch to compare against for change detection in repo mode. + :param budget: Optional repository-wide cap; stop processing once cumulative update cost reaches this amount. + :param dry_run: If True in repo mode, list pending updates only (no LLM, no prompt writes, no architecture/PRD sync). :return: Tuple containing the updated prompt, total cost, and model name. """ quiet = ctx.obj.get("quiet", False) @@ -842,7 +956,7 @@ def update_main( scan_dir = repo_root pairs = find_and_resolve_all_pairs(scan_dir, quiet, extensions, output) - if pairs: + if pairs and not dry_run: from .pddrc_initializer import ensure_pddrc_for_scan code_files_for_pddrc = [code_path for _, code_path in pairs] ensure_pddrc_for_scan(scan_dir, repo_root, code_files_for_pddrc, quiet=quiet) @@ -854,30 +968,60 @@ def update_main( # Change-detection: filter to changed code files OR empty prompts git_changed_files = get_git_changed_files(repo_root, base_branch) - changed_pairs = [] + changed_items = [] for prompt_path, code_path in pairs: # Empty prompts always need generation, regardless of code changes prompt_p = Path(prompt_path) if prompt_p.exists() and prompt_p.stat().st_size == 0: - changed_pairs.append((prompt_path, code_path)) + changed_items.append((prompt_path, code_path, "empty prompt file")) continue changed, reason = is_code_changed(code_path, repo_root, git_changed_files, prompt_file_path=prompt_path) if changed: - changed_pairs.append((prompt_path, code_path)) + changed_items.append((prompt_path, code_path, reason)) - if not changed_pairs: + if not changed_items: if not quiet: rprint("[info]No changed code files detected. Everything is in sync.[/info]") return None + if dry_run: + if not quiet: + drift_prompts = [p for p, _c, _r in changed_items] + all_prompt_paths = [p for p, _c in pairs] + included_docs = _included_docs_for_drift_report( + repo_root, all_prompt_paths, drift_prompts + ) + cost_low, cost_high = _estimate_dry_run_cost_range( + ctx, repo_obj, simple, changed_items + ) + _print_repository_drift_report( + repo_root, + len(changed_items), + len(pairs), + changed_items, + included_docs, + cost_low, + cost_high, + ) + rprint( + "[dim]No LLM calls, no prompt writes, no architecture or PRD sync.[/dim]" + ) + n = len(changed_items) + return ( + f"Dry run: {n} prompt(s) would be updated (no changes made).", + 0.0, + "N/A", + ) + if not quiet: rprint( - f"[info]Found {len(changed_pairs)} changed file(s) " + f"[info]Found {len(changed_items)} changed file(s) " f"out of {len(pairs)} total pairs.[/info]" ) results = [] total_repo_cost = 0.0 + budget_reached = False progress = Progress( SpinnerColumn(), @@ -895,11 +1039,19 @@ def update_main( with progress: task = progress.add_task( "Updating prompts...", - total=len(changed_pairs), + total=len(changed_items), total_cost=0.0 ) - for prompt_path, code_path in changed_pairs: + for prompt_path, code_path, _reason in changed_items: + if budget is not None and total_repo_cost >= budget: + budget_reached = True + if not quiet: + rprint( + f"[info]Budget cap reached (${budget:.2f}); " + f"stopping with {len(results)} file(s) processed.[/info]" + ) + break relative_path = os.path.relpath(code_path, repo_root) progress.update(task, description=f"Processing [path]{relative_path}[/path]") @@ -1032,6 +1184,8 @@ def update_main( console.print("\n[bold]Repository Update Summary[/bold]") console.print(table) + if budget_reached: + console.print(f"[info]Budget cap reached: ${budget:.2f}[/info]") if arch_entries_updated > 0 or prd_status != "skipped (no arch changes)": console.print(f"\n[info]Architecture entries updated: {arch_entries_updated}[/info]") console.print(f"[info]PRD status: {prd_status}[/info]") diff --git a/project_dependencies.csv b/project_dependencies.csv index a536f9bde..4968c4bab 100644 --- a/project_dependencies.csv +++ b/project_dependencies.csv @@ -1,258 +1,11 @@ full_path,file_summary,key_exports,dependencies,content_hash -__init__.py,"Package initialization for PDD (Prompt Driven Development) that defines global constants, sets up default cloud credentials, and re-exports key agentic orchestration and CI validation functions.","[""__version__"", ""EXTRACTION_STRENGTH"", ""DEFAULT_STRENGTH"", ""DEFAULT_TEMPERATURE"", ""DEFAULT_TIME"", ""get_agent_provider_preference"", ""get_job_deadline"", ""Pricing"", ""get_available_agents"", ""run_agentic_task"", ""run_agentic_test_orchestrator"", ""filepath_to_prompt_filename"", ""run_agentic_e2e_fix_orchestrator"", ""detect_ci_system"", ""run_agentic_e2e_fix""]","[""os""]",c1832ec1fb2c629207a5524f7443708b8e7e7858ee2b0f3ff88b7a26a2efa0dc -agentic_architecture.py,"CLI entry point that parses GitHub issue URLs, fetches issue data and comments via the `gh` CLI, ensures local repo context, and invokes an architecture orchestrator.","[""run_agentic_architecture""]","[""json"", ""re"", ""shutil"", ""subprocess"", ""pathlib"", ""typing"", ""rich""]",0751b5673560d033fc9af0027420003c2045ba9b291b5fd2f04edd2c819d0391 -agentic_architecture_orchestrator.py,"Orchestrates a multi-step agentic architecture workflow that analyzes PRDs, generates architecture files, prompts, and scaffolding with validation, retry logic, and resumable state.","[""run_agentic_architecture_orchestrator"", ""ARCH_STEP_TIMEOUTS"", ""MAX_VALIDATION_ITERATIONS""]","[""hashlib"", ""json"", ""sys"", ""pathlib"", ""typing"", ""rich"", ""pdd"", ""yaml"", ""ast"", ""re""]",4ecb50fbd7a4294b5cf16b63e2bf9d2a236986479c7b64b69e753138e2e70221 -agentic_bug.py,"CLI entry point that parses GitHub issue URLs, fetches issue data via the `gh` CLI, and invokes an orchestrator to run an agentic bug investigation workflow.","[""run_agentic_bug""]","[""json"", ""re"", ""shutil"", ""subprocess"", ""tempfile"", ""pathlib"", ""typing"", ""rich""]",613ee16d410b6bd6cd797aa28c89cadd2c7382d6097296b8a0a6fd4a8e047d06 -agentic_bug_orchestrator.py,"Orchestrates an 11-step agentic bug investigation workflow including duplicate detection, reproduction, root cause analysis, test generation, E2E verification, and PR creation using git worktrees.","[""run_agentic_bug_orchestrator"", ""detect_structural_test_patterns"", ""BUG_STEP_TIMEOUTS""]","[""logging"", ""os"", ""re"", ""shlex"", ""shutil"", ""subprocess"", ""pathlib"", ""typing"", ""rich""]",b50f65a86068fe29ea79903cf8591ad6fbc697c2bd8bb16c10d29b33309dca72 -agentic_change.py,"Provides the CLI entry point for an agentic change workflow that fetches GitHub issue details, sets up a repository, and delegates to an orchestrator.","[""run_agentic_change""]","[""json"", ""re"", ""shutil"", ""subprocess"", ""tempfile"", ""pathlib"", ""typing"", ""rich""]",cf0521fb3e74063404362aa2d1360186a123a4a7989fb4e070e8e3449e3f4ec8 -agentic_change_orchestrator.py,"Orchestrates a 13-step agentic change workflow that implements GitHub issue changes via LLM-driven steps, git worktrees, review loops, and PR creation.","[""run_agentic_change_orchestrator"", ""CHANGE_STEP_TIMEOUTS"", ""MAX_REVIEW_ITERATIONS""]","[""json"", ""os"", ""re"", ""shutil"", ""subprocess"", ""sys"", ""pathlib"", ""typing"", ""rich"", ""pdd""]",b9070919ede9ba5a1f4b790188a0b1338f5b73a749f8ccdc4072afb23e1e0696 -agentic_checkup.py,"Provides the entry point for an LLM-driven project health checkup triggered from a GitHub issue, fetching issue context and dispatching a multi-step orchestrator.","[""run_agentic_checkup""]","[""json"", ""re"", ""pathlib"", ""typing"", ""rich""]",1d7539a96f3e62c843ff97af7e7ebddf8636caab5bbefc439e16b188ec56866f -agentic_checkup_orchestrator.py,"Orchestrates an 8-step agentic checkup workflow with per-step LLM calls, iterative fix-verify loops, git worktree isolation, and resume support via persistent state.","[""run_agentic_checkup_orchestrator"", ""CHECKUP_STEP_TIMEOUTS"", ""TOTAL_STEPS"", ""STEP_ORDER"", ""MAX_FIX_VERIFY_ITERATIONS""]","[""re"", ""shutil"", ""subprocess"", ""pathlib"", ""typing"", ""rich""]",7cea5f7c3aa17e5a32b2297ef12e86e44e0c1ebe0ea240d026bc719549c251ac -agentic_common.py,"Provides shared utilities for agentic LLM workflows including multi-provider CLI orchestration, control token detection, cost calculation, GitHub state persistence, and retry logic.","[""TokenMatch"", ""Pricing"", ""detect_control_token"", ""classify_step_output"", ""substitute_template_variables"", ""get_agent_provider_preference"", ""get_available_agents"", ""run_agentic_task"", ""validate_cached_state"", ""load_workflow_state"", ""save_workflow_state"", ""clear_workflow_state"", ""post_step_comment"", ""post_pr_comment"", ""post_final_comment"", ""SEMANTIC_PATTERNS""]","[""os"", ""signal"", ""sys"", ""json"", ""shutil"", ""subprocess"", ""tempfile"", ""time"", ""uuid"", ""re"", ""random"", ""datetime"", ""pathlib"", ""dataclasses"", ""typing"", ""rich"", ""pdd"", ""yaml""]",544e8f16aa20d499b11e239918e7d4c60af5ab188e4122d64e06e74dd1221017 -agentic_crash.py,"Provides an agentic fallback for fixing program crashes by delegating to an AI agent CLI, then verifying the fix by re-running the program.","[""run_agentic_crash""]","[""json"", ""subprocess"", ""pathlib"", ""typing"", ""rich"", ""pdd""]",09d49a7bc37010ba9253be17a5cdfecaa350d7b7f8870f42256b10207c69556a -agentic_e2e_fix.py,"Provides the CLI entry point for an agentic end-to-end fix workflow that fetches GitHub issue data, resolves working directories/worktrees, and delegates to an orchestrator.","[""run_agentic_e2e_fix""]","[""json"", ""re"", ""shutil"", ""subprocess"", ""pathlib"", ""typing"", ""rich""]",4a50239f84ac2ee9991d2898c7ee4ce5344b4fe3e98d9d36f2b9af165edae080 -agentic_e2e_fix_orchestrator.py,"Orchestrates a multi-step, multi-cycle agentic workflow to diagnose and fix end-to-end test failures, with resumable state, independent test verification, git commit/push, and CI validation.","[""run_agentic_e2e_fix_orchestrator"", ""STEP_NAMES"", ""STEP_DESCRIPTIONS"", ""E2E_FIX_STEP_TIMEOUTS"", ""_classify_step_output"", ""_check_e2e_environment"", ""_commit_and_push"", ""_push_with_retry"", ""_extract_test_files"", ""_verify_tests_independently"", ""_is_intermediate_file"", ""_is_test_file"", ""_parse_changed_files"", ""_parse_dev_units"", ""_detect_changed_files""]","[""hashlib"", ""os"", ""shlex"", ""shutil"", ""subprocess"", ""sys"", ""time"", ""json"", ""datetime"", ""pathlib"", ""typing"", ""rich"", ""re"", ""urllib""]",577ffdc19117dc35c76a8acc4d90ae147150a7a0228ce7d1bfe32078e3a3b708 -agentic_fix.py,"Provides an agentic fallback mechanism that uses LLM-based agents to automatically fix code when standard fixes fail, with local test verification and file-change detection.","[""run_agentic_fix"", ""find_llm_csv_path""]","[""os"", ""shutil"", ""subprocess"", ""sys"", ""difflib"", ""pathlib"", ""typing"", ""rich"", ""re""]",88483b9cedbc1af0f1429eaed88e3f91ba151e323beee6827217ce4e56bda97f -agentic_langtest.py,"Provides language-specific test command resolution by checking a CSV file, falling back to a hardcoded Python command, or returning None to trigger agentic mode.","[""default_verify_cmd_for"", ""missing_tool_hints""]","[""csv"", ""os"", ""pathlib""]",7cb0be38d526d0a630500185427a9f76666d2b79385f7333a7608eb1fb33cfc9 -agentic_sync.py,"Orchestrates LLM-driven module identification from GitHub issues and parallel sync execution, including dry-run validation, dependency graph construction, and fingerprint-based skip detection.","[""run_agentic_sync"", ""_is_github_issue_url"", ""_detect_modules_from_branch_diff"", ""_filter_invalid_basenames"", ""_find_project_root"", ""_load_architecture_json"", ""_resolve_module_cwd"", ""_run_single_dry_run"", ""_llm_fix_dry_run_failure"", ""_run_dry_run_validation"", ""_filter_already_synced"", ""_parse_llm_response"", ""_apply_architecture_corrections"", ""_post_error_comment"", ""_is_catchall_match""]","[""json"", ""os"", ""re"", ""subprocess"", ""sys"", ""pathlib"", ""typing"", ""rich"", ""fnmatch""]",c6d2c1944df2fdf9a11208d6a0b400ec1abbd2947102e3edfb17a8081cae22f7 -agentic_sync_runner.py,"Manages parallel execution of `pdd sync` subprocesses with dependency-aware scheduling, resumable state persistence, and live GitHub issue comment progress updates.","[""AsyncSyncRunner"", ""ModuleState"", ""build_dep_graph_from_architecture"", ""MAX_WORKERS"", ""MODULE_TIMEOUT"", ""STATE_FILE_PATH""]","[""csv"", ""datetime"", ""json"", ""os"", ""re"", ""signal"", ""subprocess"", ""sys"", ""tempfile"", ""threading"", ""time"", ""concurrent"", ""dataclasses"", ""pathlib"", ""typing"", ""rich""]",38360cfa1f1c4267a6894393bfaa6e31662082b12ce321cc8a4ea3a90c2484a9 -agentic_test.py,CLI entry point that fetches GitHub issue data and invokes an orchestrator to run an agentic test generation workflow.,"[""run_agentic_test"", ""main"", ""agentic_test_main""]","[""argparse"", ""json"", ""re"", ""shutil"", ""subprocess"", ""sys"", ""tempfile"", ""pathlib"", ""typing"", ""urllib"", ""rich""]",e75623979763f3d77f6635c5f3c667947e3cf71634ed73100172735c119bb2ee -agentic_test_generate.py,"Provides agentic test generation for non-Python codebases by delegating to an agentic CLI that explores projects, discovers test frameworks, generates tests, and verifies they pass.","[""run_agentic_test_generate""]","[""json"", ""re"", ""pathlib"", ""typing"", ""rich"", ""pdd.agentic_common"", ""pdd.load_prompt_template""]",0a4f609fe31e50764258ad9f8d983263db3bd634bcc74c10fb5815ace065f621 -agentic_test_orchestrator.py,"Orchestrates an 18-step agentic test generation workflow with git worktree isolation, state persistence, resumability, cost tracking, and optional Playwright-based manual browser testing.","[""run_agentic_test_orchestrator"", ""TEST_STEP_TIMEOUTS""]","[""json"", ""os"", ""re"", ""shutil"", ""subprocess"", ""time"", ""pathlib"", ""typing"", ""rich""]",fc556f7e5b91793c30e298ca7a48f315da7c0ce844993f063e8fc1b947fb1192 -agentic_update.py,"Coordinates agentic updates of prompt files by discovering test files, invoking an external LLM CLI agent, and detecting file changes via mtime comparison.","[""run_agentic_update""]","[""pathlib"", ""typing"", ""os"", ""traceback"", ""rich""]",44cdc5df1f004abd5fb889137689ee02d531a3c3f73be724833dd129993ca9bf -agentic_verify.py,"Runs an agentic verification fallback that delegates code verification fixes to a CLI agent, tracks file changes, and parses structured JSON results.","[""run_agentic_verify""]","[""json"", ""os"", ""re"", ""pathlib"", ""typing"", ""rich""]",749a347da333213e539364b8ac165c931a25b206cc958dbfb4fdc781b4692fbc -api_key_scanner.py,"Discovers API keys required by user-configured LLM models and checks their availability across .env files, shell environment, and PDD config sources.","[""KeyInfo"", ""get_provider_key_names"", ""scan_environment""]","[""csv"", ""logging"", ""os"", ""dataclasses"", ""pathlib"", ""typing""]",53406b4d8f8db4115e341fdb9fe941f955d11e6196680c64117854cdcefb2db8 -architecture_registry.py,"Manages an architecture registry that tracks multi-issue code generation provenance, including loading/saving registry state, recording generation events, and merging new architecture entries with existing ones.","[""load_registry"", ""save_registry"", ""record_generation"", ""merge_architecture"", ""find_architecture_for_project"", ""get_modules_for_issue""]","[""json"", ""datetime"", ""pathlib"", ""typing""]",bc01a1400b40d6a64f879ef92d8b8fc3d2fbb17f5209cfe6af5e05f0d4782379 -architecture_sync.py,"Provides bidirectional synchronization between PDD prompt files (containing XML metadata tags) and architecture.json, including tag parsing, architecture updates, dependency validation, and interface signature merging.","[""parse_prompt_tags"", ""normalize_architecture_filenames"", ""update_architecture_from_prompt"", ""sync_all_prompts_to_architecture"", ""register_untracked_prompts"", ""validate_dependencies"", ""validate_interface_structure"", ""get_architecture_entry_for_prompt"", ""has_pdd_tags"", ""generate_tags_from_architecture"", ""ARCHITECTURE_JSON_PATH"", ""PROMPTS_DIR""]","[""ast"", ""json"", ""re"", ""pathlib"", ""typing"", ""lxml""]",02c15ebbb1f0295371b9cef30057c83c00fc657510fdce4373753e82e8acad8e -architecture_sync_helper.py,Provides a utility function to convert output filepaths into normalized prompt filenames that mirror the source directory structure.,"[""filepath_to_prompt_filename""]","[""os"", ""pathlib""]",48dfe1f34944a6c956aab3d9be722a98240d0052cfcaaf5051e63e67a8720e86 -auth_service.py,"Provides shared authentication services for managing JWT cache files, keyring-stored refresh tokens, auth status checks, token verification, and logout across CLI and REST API interfaces.","[""JWT_CACHE_FILE"", ""KEYRING_SERVICE_NAME"", ""KEYRING_USER_NAME"", ""get_jwt_cache_info"", ""get_cached_jwt"", ""has_refresh_token"", ""clear_jwt_cache"", ""clear_refresh_token"", ""get_auth_status"", ""get_refresh_token"", ""verify_auth"", ""logout""]","[""json"", ""time"", ""pathlib"", ""typing"", ""keyring"", ""keyrings"", ""base64"", ""os""]",0ebbbe6467ba43efe30ebcd153eb7c84314bd3b61b8e62db9feae097af45b1f8 -auto_deps_main.py,Provides the main entry point for analyzing a prompt file and automatically inserting dependency includes found in a specified directory.,"[""auto_deps_main""]","[""sys"", ""pathlib"", ""typing"", ""click"", ""rich"", ""filelock""]",007b80405bcee979fc7b5bd5c225b955422f1381fa7126e3988bf7514e71eb66 -auto_include.py,Provides the `auto_include` function that uses LLM-driven analysis of a directory's file summaries to automatically generate context-aware include directives with selective extraction for prompt templates.,"[""auto_include"", ""AutoIncludeResult"", ""NewInclude"", ""IncludeAnnotation""]","[""os"", ""re"", ""io"", ""pathlib"", ""typing"", ""pandas"", ""pydantic"", ""rich""]",90e3b24791930ef19c2c27ddfc86b79427aec265fba5dcf1242af18741f432f2 -auto_update.py,"Provides automatic package update functionality by detecting the installer (UV or pip), checking PyPI for newer versions, and performing interactive upgrades.","[""detect_installation_method"", ""get_upgrade_command"", ""auto_update""]","[""importlib"", ""os"", ""shutil"", ""subprocess"", ""sys"", ""typing"", ""requests"", ""semver""]",0945c48a916c2783701c5aac8da7e2f033c4a2c0939f1cb96dab30bdb39eadba -bug_main.py,"Generates unit tests from observed vs. desired program output, supporting both cloud API and local execution with automatic fallback.","[""bug_main""]","[""json"", ""os"", ""sys"", ""typing"", ""click"", ""requests"", ""pathlib"", ""rich""]",3cb5ac48f84e5e573afc52653fee671966d6718a309d1ebcb7bbd5dab433dea7 -bug_to_unit_test.py,Generates unit tests from bug reports by invoking an LLM with bug details (current vs. desired output) and post-processing the result.,"[""bug_to_unit_test"", ""main""]","[""typing"", ""rich""]",06842a372290a414035bcf89c011ff457e111d14a253c2a4dc4a6516b9040add -change.py,Modifies an existing prompt based on change instructions by invoking an LLM and extracting the resulting modified prompt via delimiters or a fallback LLM extraction step.,"[""change"", ""extract_between_delimiters"", ""ExtractedPrompt"", ""MODIFIED_PROMPT_START"", ""MODIFIED_PROMPT_END"", ""main""]","[""typing"", ""rich"", ""pydantic""]",d66d22f9ef9e6a80574d91b115bf72aac534d238dd8a8085d5744e1a8c1562a3 -change_main.py,"Implements the main handler for a 'change' command that modifies prompt files based on change instructions and code context, supporting single-file and CSV batch modes.","[""change_main""]","[""csv"", ""logging"", ""os"", ""pathlib"", ""typing"", ""click"", ""rich""]",7ab54b609a12e314ce3cdb14ad27e41b35c8cb69dfef3dafab6368eef52f4e90 -ci_validation.py,"Polls GitHub PR required CI checks, collects failure logs, and iteratively invokes an agentic task to fix CI failures until checks pass or retries are exhausted.","[""detect_ci_system"", ""post_ci_failure_comment"", ""run_ci_validation_loop""]","[""io"", ""json"", ""subprocess"", ""time"", ""zipfile"", ""pathlib"", ""typing"", ""rich""]",722be97227237d70273fda27302dce83056eedc18d3b2d48fcef87ba1d4977f5 -cli.py,"Defines the CLI entry point for the PDD tool, bootstrapping package defaults, registering commands, and re-exporting key command modules for backward compatibility.","[""cli"", ""register_commands"", ""templates_group"", ""auto_update"", ""code_generator_main"", ""context_generator_main"", ""cmd_test_main"", ""fix_main"", ""split_main"", ""change_main"", ""update_main"", ""sync_main"", ""auto_deps_main"", ""detect_change_main"", ""process_commands""]","[""os"", ""sys""]",3a841cf62b08ffc814f9d7e05278cc9967ecc9ad80e40c44b70ce6a72bf530fd -cli_detector.py,"Detects, installs, and configures AI coding CLI tools (Claude, Gemini, Codex) including API key management and shell RC file integration.","[""CliBootstrapResult"", ""detect_and_bootstrap_cli"", ""detect_cli_tools"", ""PROVIDER_PRIMARY_KEY"", ""PROVIDER_DISPLAY"", ""CLI_PREFERENCE"", ""SHELL_RC_MAP"", ""console""]","[""os"", ""shutil"", ""subprocess"", ""sys"", ""dataclasses"", ""pathlib"", ""typing"", ""rich"", ""pdd""]",b74a422e1f7277a3b744530c9d4ff918654c94a18817a1276ec438ee161bf77f -cmd_test_main.py,"Implements the main CLI entry point for generating or enhancing unit tests, supporting cloud and local execution modes, agentic generation for non-Python languages, and coverage-driven test increase.","[""cmd_test_main"", ""main""]","[""json"", ""os"", ""pathlib"", ""click"", ""requests"", ""rich""]",2a38c07c2bb7d8d3b4eb895a2a0b7e628ffc3c53281e1d918d13a554c07846cd -code_generator.py,"Orchestrates multi-step code generation from a prompt using an LLM, including preprocessing, invocation, completion checking, continuation, and postprocessing.","[""code_generator""]","[""json"", ""re"", ""typing"", ""rich""]",b2de6272f21a7f0d0754d8a82c70bb4171a7390ad3a6036983477d0caf6e98a8 -code_generator_main.py,"CLI wrapper that orchestrates code generation from prompt files, supporting full and incremental modes, local and cloud execution, git-based diffing, post-processing hooks, and architecture conformance validation.","[""code_generator_main"", ""is_git_repository"", ""get_git_content_at_ref"", ""get_file_git_status"", ""git_add_files"", ""_parse_front_matter"", ""_expand_vars"", ""_verify_architecture_conformance"", ""_detect_wireable_exports"", ""_wire_to_parent_init"", ""_find_default_test_files"", ""_repair_architecture_interface_types""]","[""ast"", ""os"", ""re"", ""json"", ""pathlib"", ""shlex"", ""subprocess"", ""requests"", ""tempfile"", ""sys"", ""typing"", ""click"", ""rich""]",baa627c527002fcc7d8544c860df75a296bbeca8115abaa6eabbe3f75b6774da -commands/__init__.py,Registers all CLI subcommands and command groups onto the main Click CLI group from various submodules.,"[""register_commands""]","[""click""]",e96bb4420aaac41923d3af5e6adededc2350be663eb6f6687ed6581f815124c9 -commands/analysis.py,"Defines Click CLI commands for analysis operations: detect-change, conflicts, bug, crash, and trace, delegating to their respective main modules.","[""detect_change"", ""conflicts"", ""bug"", ""crash"", ""trace"", ""get_context_obj""]","[""os"", ""click"", ""typing""]",a7aa881a4c71a655a1d68fb2c8fa17f3840280277061b6f513856b5a830cdb53 -commands/auth.py,"Defines a Click CLI command group for managing PDD Cloud authentication, including login via GitHub, status checking, logout, token retrieval, and cache clearing.","[""auth_group"", ""login"", ""status"", ""logout_cmd"", ""token_cmd"", ""clear_cache""]","[""asyncio"", ""base64"", ""json"", ""os"", ""sys"", ""time"", ""pathlib"", ""typing"", ""click"", ""rich""]",c00c337761b39d83eaac3cf1ce677ad3b4338874c76513e7935b16ed917a357a -commands/checkup.py,"Defines the CLI ""checkup"" command that runs an agentic health check on a PDD project using a GitHub issue URL as input.","[""checkup""]","[""click"", ""typing""]",0acb4adfb56f35e123112dfed0893da6f0bcc66c4c89f22b66d35309baef60b2 -commands/connect.py,"Implements the `pdd connect` CLI command that launches a local REST server with smart port detection, optional cloud session registration, and browser auto-opening for the PDD web frontend.","[""connect"", ""is_port_available"", ""find_available_port"", ""DEFAULT_PORT"", ""PORT_RANGE_START"", ""PORT_RANGE_END""]","[""asyncio"", ""errno"", ""os"", ""socket"", ""webbrowser"", ""pathlib"", ""typing"", ""click"", ""uvicorn""]",5e48d035fd51562df43455193c03d79e64cd34dbfbe0f7595cbff12f97e10e61 -commands/extracts.py,Re-exports the `extracts` cache management command from the `extracts_prune` module.,"[""extracts""]",[],a60aaa708858c783aa49a73bf9513a1939650afba96acfdee80ee3b2228deb3c -commands/firecrawl.py,"Defines CLI commands for inspecting, clearing, and managing the Firecrawl web scraping cache, including stats, configuration info, and URL lookup.","[""firecrawl_cache_group"", ""firecrawl_cache""]","[""os"", ""click"", ""rich""]",ef4b2875252e86550cbe6c87e07a3662edff352f87c3affdbf0393146833be7f -commands/fix.py,"Defines the `fix` CLI command that routes to agentic issue fixing, user-story-driven fixing, or manual code/test fixing based on the provided arguments.","[""fix""]","[""re"", ""sys"", ""pathlib"", ""typing"", ""click"", ""rich""]",41de5f9a2710d66eca4fd5001e52f981372af2a142d99c639f15c5ab5b292c92 -commands/generate.py,"Defines CLI commands (`generate`, `example`, `test`) for code generation, example creation, and test generation/story linking using Click.","[""GenerateCommand"", ""generate"", ""example"", ""test""]","[""os"", ""re"", ""sys"", ""click"", ""pathlib"", ""typing"", ""rich""]",2d1b242b50d33332140740da5ed44a60c6a3471bada336f9d983bd21999c41ed -commands/maintenance.py,"Defines CLI commands for syncing prompts with code/tests, analyzing project dependencies, and running interactive setup.","[""sync"", ""auto_deps"", ""setup""]","[""click"", ""typing"", ""pathlib""]",59b4c9437f8c0e3259fa05123375c442e5fb998da9e4c5246c1d4e0118d21d2d -commands/misc.py,"Defines a Click CLI command for preprocessing prompt files with options for XML delimiters, recursive includes, bracket doubling, and PDD metadata injection.","[""preprocess""]","[""click"", ""typing""]",8bd18ad30b68fb7a25781d2d4b072c1e2a65f5286cdaf119e34fa952b757d12e -commands/modify.py,"Defines CLI commands (split, change, update) using Click for prompt-driven development operations including splitting prompts, applying changes via manual or agentic modes, and updating prompts from code changes.","[""split"", ""change"", ""update""]","[""sys"", ""pathlib"", ""typing"", ""click"", ""rich""]",3d64352418953f1f199c20310f61128bee8cb69dd3f9146a9ae850a47d7c66d6 -commands/report.py,"Defines the `report-core` CLI command that creates a GitHub issue from a PDD core dump file, either via the GitHub API or by opening a browser.","[""report_core""]","[""os"", ""click"", ""json"", ""webbrowser"", ""urllib"", ""pathlib"", ""typing""]",c07ff974784d7f8f08b464e4326f6e22bb1250945411531dc3860cd7609f5c35 -commands/sessions.py,"Defines CLI commands for listing, inspecting, and cleaning up remote PDD sessions using Click and Rich.","[""sessions"", ""list_sessions"", ""session_info"", ""cleanup_sessions""]","[""asyncio"", ""json"", ""typing"", ""click"", ""rich""]",8b05a311e2ad2ef499eb6c97b4f68308f659f87976770181eb0b946a41012266 -commands/templates.py,"Defines a CLI command group for listing, showing details of, and copying project templates using Click and Rich.","[""templates_group"", ""templates_list"", ""templates_show"", ""templates_copy""]","[""io"", ""click"", ""rich"", ""typing""]",6cb72959c02bcb9df6c2661ed339d61a782f4466980c0f899371af2113818dd0 -commands/utility.py,"Defines CLI commands for installing shell completion and running code verification with configurable attempts, budget, and agentic fallback.","[""install_completion_cmd"", ""verify""]","[""click"", ""typing""]",2663395441a7a36c81de2c64d6c650dc43704b7dc3294fc08eb30eb5efeb9eee -commands/which.py,"Implements the `pdd which` CLI command that displays resolved configuration values, search paths, and effective settings for prompt/example/test/output directories.","[""which""]","[""click"", ""inspect"", ""os"", ""pathlib"", ""json"", ""typing""]",98e41f86fc822f8bde745d8c8f0bdb00aec2414ad0db6985ed5f033118b3d63c -comment_line.py,"Provides a utility function to comment out a line of code using language-specific comment characters, supporting single, encapsulating, and deletion styles.","[""comment_line""]",[],b17da17f86c6d5f20b8bfaef23d324930099d888ad80a916ee73a2c3c69028f6 -config_resolution.py,"Provides centralized resolution of configuration values (strength, temperature, time) with a defined priority order across CLI options, pddrc defaults, and hardcoded fallbacks.","[""resolve_effective_config""]","[""typing"", ""click""]",0e35930c0ee715ec47e44da1ced3c4149772d1a6e86bc226341323e1cccf7a23 -conflicts_in_prompts.py,Analyzes two prompts for conflicts using LLM calls and returns structured resolution suggestions as a list of changes.,"[""ConflictChange"", ""ConflictResponse"", ""conflicts_in_prompts"", ""main""]","[""typing"", ""pydantic"", ""rich""]",f4ddeb6567495066b23933a09c75bd1bf266d42f46e18f2148b8675117350646 -conflicts_main.py,"Analyzes conflicts between two prompt files using an LLM, then outputs detected conflicts and resolution instructions to a CSV file.","[""conflicts_main""]","[""csv"", ""sys"", ""typing"", ""click"", ""rich""]",4a022e53c77d4bc1aa3f411e7e5a00f364eeb6ef3d9f976171f3c2b697f83dc8 -construct_paths.py,"Resolves input/output file paths, detects project context from `.pddrc` configuration, determines language/extension, and orchestrates overwrite confirmation for PDD commands.","[""BUILTIN_EXT_MAP"", ""construct_paths"", ""detect_context_for_file"", ""list_available_contexts"", ""resolve_effective_config"", ""get_tests_dir_from_config"", ""get_language_outputs""]","[""sys"", ""os"", ""pathlib"", ""typing"", ""fnmatch"", ""logging"", ""click"", ""yaml"", ""rich"", ""csv""]",675f3b2b3b7daeac3b3eac44d97458c3b6238c1915c487ae38f70954681a40e6 -content_selector.py,"Provides precise content extraction from files using line ranges, Python AST selectors, Markdown sections, regex patterns, and JSON/YAML path traversal.","[""ContentSelector"", ""SelectorError""]","[""ast"", ""json"", ""re"", ""textwrap"", ""dataclasses"", ""typing"", ""rich"", ""yaml"", ""signal""]",ad19d58df177ac07cc8f06e3a85d48301770e3690902e90113b45c0cceddaf1a -context_generator.py,"Orchestrates an LLM pipeline to generate concise usage examples for code modules, handling prompt loading, preprocessing, invocation, completion detection, and postprocessing.","[""context_generator""]","[""rich"", ""typing""]",f972e2e2ce7444b7d3d186ab4540b8cad4f66dd0d97d37df00c58f32a9e3e369 -context_generator_main.py,"Generates code examples from prompts and source code via cloud API with local fallback, including Python syntax validation and repair.","[""context_generator_main"", ""CLOUD_TIMEOUT_SECONDS""]","[""ast"", ""asyncio"", ""json"", ""os"", ""sys"", ""pathlib"", ""typing"", ""click"", ""httpx"", ""rich""]",2e9c07e181b501054781d190822af592980ee8ec5b7bb6f6f026bcfa7725766e -continue_generation.py,"Iteratively continues LLM text generation, trimming and appending output in a loop until the result is detected as complete or a maximum iteration limit is reached.","[""continue_generation"", ""TrimResultsStartOutput"", ""TrimResultsOutput"", ""MAX_GENERATION_LOOPS""]","[""typing"", ""logging"", ""rich"", ""pydantic""]",d72a8e5da32095248b526cc5d59f465cf4b4f74c9f7eb0d2d31acb8ee64235fc -core/__init__.py,"Re-exports cloud configuration classes, error types, and environment variable constants for the PDD CLI core utilities package.","[""CloudConfig"", ""AuthError"", ""NetworkError"", ""TokenError"", ""UserCancelledError"", ""RateLimitError"", ""FIREBASE_API_KEY_ENV"", ""GITHUB_CLIENT_ID_ENV"", ""PDD_CLOUD_URL_ENV"", ""PDD_JWT_TOKEN_ENV"", ""DEFAULT_BASE_URL"", ""CLOUD_ENDPOINTS""]","["".cloud""]",9b9cb024c55bd8f8661dbc42d4a300f9360fec09b8675aeffc38a180e6510aed -core/cli.py,"Defines the main CLI entry point and Click command group for the PDD tool, handling global options, output capture, error handling, and command execution summaries.","[""cli"", ""PDDCLI"", ""OutputCapture"", ""process_commands""]","[""os"", ""sys"", ""io"", ""re"", ""click"", ""typing""]",8be1342695e0e6e8aebd453ae921263823ba775b064422667c2b49008f212aa3 -core/cloud.py,"Provides centralized cloud URL configuration, JWT authentication, and endpoint management for PDD CLI commands.","[""CloudConfig"", ""AuthError"", ""NetworkError"", ""TokenError"", ""UserCancelledError"", ""RateLimitError"", ""FIREBASE_API_KEY_ENV"", ""GITHUB_CLIENT_ID_ENV"", ""PDD_CLOUD_URL_ENV"", ""PDD_JWT_TOKEN_ENV"", ""PDD_CLOUD_TIMEOUT_ENV"", ""DEFAULT_BASE_URL"", ""DEFAULT_CLOUD_TIMEOUT"", ""CLOUD_ENDPOINTS"", ""get_cloud_timeout""]","[""asyncio"", ""os"", ""typing"", ""rich""]",6a4fbb62e3331bc30e582a51ff8de8385b1e202ceb8f0dbd30b013abec9ca555 -core/dump.py,"Provides core dump generation, garbage collection, GitHub issue/gist reporting, and replay script creation for the PDD CLI debugging workflow.","[""garbage_collect_core_dumps"", ""_write_core_dump"", ""_get_github_token"", ""_github_config"", ""_create_gist_with_files"", ""_post_issue_to_github"", ""_write_replay_script"", ""_build_issue_markdown""]","[""os"", ""sys"", ""json"", ""platform"", ""datetime"", ""shlex"", ""subprocess"", ""pathlib"", ""typing"", ""click"", ""requests""]",bc469eb9b961867a3fa1760f0e4799cbf3576f37e496f428572af64000f6b746 -core/errors.py,"Provides centralized error handling, Rich console formatting, and core-dump error collection for the PDD CLI tool.","[""handle_error"", ""get_core_dump_errors"", ""clear_core_dump_errors"", ""console"", ""custom_theme""]","[""os"", ""traceback"", ""typing"", ""click"", ""rich"", ""pdd""]",82f2f58cea89896ad6b017b16fdb49aed7154a1be2129e1dd2e76de93bf3feda -core/remote_session.py,Provides utility functions to detect remote/SSH/headless sessions and determine whether to skip opening a browser.,"[""is_remote_session"", ""should_skip_browser""]","[""os"", ""sys"", ""typing""]",578e98f63ccacd49af41b7d17a31460a2eedffd8648e9fcaefc54fc17d0d3ffb -core/utils.py,"Provides helper functions for the PDD CLI, including onboarding reminder logic, environment detection, and setup utility execution.","[""_first_pending_command"", ""_api_env_exists"", ""_completion_installed"", ""_project_has_local_configuration"", ""_should_show_onboarding_reminder"", ""_run_setup_utility""]","[""os"", ""sys"", ""subprocess"", ""pathlib"", ""typing"", ""click""]",23698ae3ce031404ca5479fd8275897dae921f2cea9c94b4cf473f2802f2db6d -crash_main.py,"Provides the main entry point for fixing crashed code modules and their calling programs, supporting both cloud and local execution with optional iterative looping.","[""crash_main""]","[""sys"", ""typing"", ""json"", ""click"", ""rich"", ""pathlib"", ""requests"", ""os""]",d31c30d7f77f324549cc0bf319732c472382380e0e386337552accb19ce721ac -data/language_format.csv,"CSV configuration file mapping programming languages to their comment syntax, file extensions, run/test commands, and supported output types.",[],[],db3542c10a4c5c2a4a9bbe02558b4bd02fbb357fb243e33de4831d337484373e -data/llm_model.csv,"CSV configuration file cataloging LLM models across various providers with pricing, API key requirements, capability flags, and reasoning type settings.",[],[],2127af1548dc49727087b65a3b2b700b13dd606230831a5ec79c59122725c6d9 -detect_change.py,"Analyzes prompt files using an LLM to detect which ones need modifications based on a natural-language change description, returning structured change instructions.","[""detect_change"", ""ChangeInstruction"", ""ChangesList""]","[""typing"", ""pathlib"", ""rich"", ""pydantic""]",980ea4eb6c6a794d541bffc28f3bde24ad090e24403bb0000be2547d2d22d874 -detect_change_main.py,"CLI wrapper that analyzes which prompts need modification based on a change description, outputting results to CSV and providing user feedback.","[""detect_change_main""]","[""csv"", ""sys"", ""typing"", ""click"", ""rich""]",6847a188594a01fa9e816e9ed7c81530ac5b71242957d82a2e9c97ec197b0a1c -docs/prompting_guide.md,"A comprehensive guide for writing effective prompts in Prompt-Driven Development (PDD), covering syntax, structure, grounding, modularity, and best practices contrasted with interactive patching tools.",[],[],4c8cb2d0534a3e7a7520447b0d461ccdd4daa5071228bfd7cfada3d6191ac630 -edit_file.py,Provides a LangGraph-based workflow that uses Claude 3.7 Sonnet and MCP tools to edit files based on natural language instructions.,"[""EditFileState"", ""edit_file"", ""run_edit_in_subprocess"", ""graph"", ""calculate_hash"", ""read_file_content"", ""write_file_content"", ""start_editing"", ""plan_edits"", ""execute_edit"", ""handle_error"", ""decide_next_step"", ""check_edit_result"", ""format_tools_for_claude""]","[""asyncio"", ""json"", ""os"", ""hashlib"", ""logging"", ""subprocess"", ""sys"", ""typing"", ""aiofiles"", ""pathlib"", ""importlib"", ""langgraph"", ""langchain_core"", ""pydantic"", ""langchain_anthropic"", ""langchain"", ""langchain_community"", ""langchain_mcp_adapters""]",f85859f8a18a6243db9edd331620e79e8b0f2e1ddb6ca99fb5e9f4228b38fbdd -extracts_prune.py,"Provides CLI commands for managing the `.pdd/extracts/` cache directory, including pruning orphaned extract entries no longer referenced by any prompt file.","[""extracts"", ""parse_include_tags"", ""prune""]","[""hashlib"", ""json"", ""os"", ""re"", ""pathlib"", ""click"", ""pdd""]",b980825f40ef418c29bbeffba0dfa86ee9646f44775dc5f21e3f33b542a5ede0 -find_section.py,"Parses lines of text to locate markdown-style fenced code block sections, returning their language, start, and end indices.","[""find_section""]",[],973fc53d8e0a0c24401a52f5a565598aeb5436fec4e0ab0314afb2983580fc47 -firecrawl_cache.py,"Provides a persistent SQLite-based cache for Firecrawl web scraping results with configurable TTL, URL normalization, LRU eviction, and automatic size management to reduce API usage.","[""FirecrawlCache"", ""get_firecrawl_cache"", ""clear_firecrawl_cache"", ""get_firecrawl_cache_stats""]","[""os"", ""hashlib"", ""time"", ""sqlite3"", ""json"", ""pathlib"", ""typing"", ""logging""]",4b5ffcf7246451bf1c60f86594dda44e4eeeb89778a56e0a400ba35dd17e1677 -fix_code_loop.py,"Implements an iterative fix loop that runs a verification program, detects errors, and uses LLM (local or cloud) or agentic fallback to automatically fix code and test files.","[""fix_code_loop"", ""cloud_crash_fix"", ""run_process_with_output"", ""ProcessResult"", ""CLOUD_REQUEST_TIMEOUT"", ""CLOUD_AVAILABLE""]","[""json"", ""os"", ""shutil"", ""subprocess"", ""sys"", ""threading"", ""pathlib"", ""typing"", ""requests"", ""rich"", ""datetime""]",d30569365bc1d9090df8f83f2cd05aa03b66b3a8c76fb30e110351260a3a8680 -fix_code_module_errors.py,Uses a two-stage LLM pipeline to analyze code errors and extract fixes for both a program and its associated code module.,"[""CodeFix"", ""fix_code_module_errors"", ""validate_inputs""]","[""typing"", ""pydantic"", ""rich"", ""json""]",cdb5b1681cc0629f6bd28c19be5378e9a005280d606723a366a5fabc2133ed89 -fix_error_loop.py,"Implements an iterative fix loop that runs tests, calls an LLM (local or cloud) to fix failing code/tests, and supports agentic fallback for non-Python languages.","[""fix_error_loop"", ""cloud_fix_errors"", ""run_pytest_on_file"", ""format_log_for_output"", ""escape_brackets""]","[""os"", ""sys"", ""subprocess"", ""shutil"", ""json"", ""datetime"", ""pathlib"", ""typing"", ""requests"", ""rich""]",090ceeee20206dc9468fb7c1a37c215df906e36aa6bd11a902c08e1c86160899 -fix_errors_from_unit_tests.py,"Uses LLM invocations to analyze unit test failures, generate fixes for both test and source code, and log error diagnostics to a file.","[""CodeFix"", ""fix_errors_from_unit_tests"", ""validate_inputs"", ""write_to_error_file""]","[""os"", ""tempfile"", ""datetime"", ""typing"", ""pydantic"", ""rich""]",52bdcb6467aefd0b24c86bab040dc80b93f2f701fbadd984f41dca3096f2e633 -fix_main.py,"Implements the main error-fixing workflow for code and unit tests, supporting both cloud-based and local execution modes with optional iterative loop fixing and auto-submission of successful fixes.","[""fix_main""]","[""sys"", ""typing"", ""json"", ""click"", ""rich"", ""requests"", ""asyncio"", ""os"", ""pathlib""]",e3d5cc748f7f77c6ebf6acddeba0edf291e4f2c70eac93cbb5f7c140b8be22ee -fix_verification_errors.py,Verifies LLM-generated code against expected output and automatically fixes identified issues using a two-step LLM pipeline.,"[""VerificationOutput"", ""FixerOutput"", ""fix_verification_errors""]","[""typing"", ""rich"", ""pydantic""]",cad19abf3b71dff60e08da64db4f3332d29ffdda0beb62be2784e207c1aea67f -fix_verification_errors_loop.py,"Implements an iterative loop that runs a program, detects verification errors, calls an LLM (local or cloud) to fix code, validates fixes via secondary verification, and falls back to agentic verification if needed.","[""fix_verification_errors_loop"", ""cloud_verify_fix""]","[""json"", ""os"", ""shutil"", ""subprocess"", ""datetime"", ""sys"", ""pathlib"", ""typing"", ""xml"", ""time"", ""requests"", ""rich""]",09107bece2783f708c5bfe5b2b0ba594cd9b9411184444440158c689b7d6f1b8 -fix_verification_main.py,"Provides CLI verification logic that runs a program, compares its output against a prompt using an LLM (locally or via cloud), and optionally iterates to fix code discrepancies.","[""run_program"", ""fix_verification_main"", ""DEFAULT_TEMPERATURE"", ""DEFAULT_MAX_ATTEMPTS"", ""DEFAULT_BUDGET"", ""VERIFICATION_PROGRAM_NAME""]","[""sys"", ""os"", ""subprocess"", ""click"", ""logging"", ""json"", ""pathlib"", ""typing"", ""requests"", ""rich""]",3db2b6757cfa29a5f56fe8d98ab2b588d0ca6b45fedfac8a2c91cec1af16dd7b -frontend/.gitignore,"A .gitignore configuration file that excludes dependency, environment, build cache, and IDE files from version control.",[],[],161f852297d7e1b1e5bcc9a7f93e5527ed2958bfc994314cb4235edd9cae0994 -frontend/App.tsx,"Main React application component providing a dashboard UI for managing dev prompts, executing commands (sync, bug, fix, change) locally or on remote sessions, and tracking jobs and task queues.","[""App""]","[""react"", ""./types"", ""./constants"", ""./lib/commandBuilder"", ""./components/PromptSelector"", ""./components/PromptSpace"", ""./components/ArchitectureView"", ""./components/ProjectSettings"", ""./components/JobDashboard"", ""./components/TaskQueuePanel"", ""./components/AddToQueueModal"", ""./components/AuthStatusIndicator"", ""./components/ReauthModal"", ""./components/ErrorBoundary"", ""./components/RemoteSessionSelector"", ""./components/ExecutionModeToggle"", ""./components/DeviceIndicator"", ""./api"", ""./components/Icon"", ""./hooks/useJobs"", ""./hooks/useTaskQueue"", ""./components/Toast"", ""./hooks/useAudioNotification""]",6fe0a65499264a74e5c2af0bf57d7ff58b5ee31983dd9686ea6bf961df7c5c3e -frontend/README.md,README providing instructions for locally running and deploying an AI Studio app using Node.js and a Gemini API key.,[],[],d70d4fa4368292fcef88ea486fcc0fa034a8e964b7143377aef08c2976423e18 -frontend/api.ts,"TypeScript API client for the PDD server, providing methods for command execution, file operations, prompt analysis, architecture management, authentication, remote sessions, and WebSocket streaming.","[""api"", ""PDDApiClient"", ""ServerStatus"", ""CommandInfo"", ""JobHandle"", ""JobResult"", ""FileTreeNode"", ""FileContent"", ""PromptInfo"", ""CommandRequest"", ""RunResult"", ""TokenMetrics"", ""SyncStatus"", ""ModelInfo"", ""DiffAnalysisResult"", ""DiffAnalysisRequest"", ""DiffAnalysisResponse"", ""ArchitectureModule"", ""ArchitectureValidationResult"", ""ArchitectureSyncResult"", ""GenerateArchitectureRequest"", ""GeneratePromptFromArchRequest"", ""BatchGeneratePromptsRequest"", ""RemoteSessionInfo"", ""RemoteCommandRequest"", ""RemoteCommandStatus"", ""GenerationGlobalOptions"", ""PromptHistoryResponse"", ""PromptDiffResponse"", ""AuthStatus""]","[""yaml""]",8b4d2091de5e9332031d7463c839a1999257c4d0816627d2088ae5279aa3f74b -frontend/components/AddModuleModal.tsx,"React modal component for adding a new architecture module with form fields for name, language, filepath, description, priority, tags, interface type, and dependencies.","[""AddModuleModal"", ""LANGUAGES"", ""INTERFACE_TYPES""]","[""react"", ""../api"", ""./FilePickerInput""]",b2da8573c2bfacb7b00cf148c6abe8e5dd7c824a9f2530128dc4473421a9c09f -frontend/components/AddToQueueModal.tsx,React modal component that allows users to configure command options and add tasks to an execution queue for later processing.,"[""AddToQueueModal""]","[""react"", ""../api"", ""../types"", ""../constants"", ""../lib/commandBuilder"", ""./FilePickerInput""]",95231589e123aab5c38665df9115248db4e2c3ce745c9f685bce174de9d3756f -frontend/components/ArchitectureView.tsx,"React component that provides a full architecture management UI with dependency graph visualization, prompt generation, module editing, batch syncing, and remote execution support.","[""ArchitectureView"", ""moduleNeedsSync""]","[""react"", ""../api"", ""./DependencyViewer"", ""./FileBrowser"", ""./GenerationProgressModal"", ""./PromptOrderModal"", ""./GraphToolbar"", ""./ModuleEditModal"", ""./AddModuleModal"", ""./GroupEditModal"", ""./SyncFromPromptModal"", ""./SyncOptionsModal""]",552bbaf7017ca2ede2fecc6569fff14e686f4f98874c5600093e925110835f6e -frontend/components/AuthStatusIndicator.tsx,"React component that displays and polls cloud authentication status, showing a styled indicator button that triggers re-authentication.","[""AuthStatusIndicator""]","[""react"", ""../api""]",f88fa207cfff5b26b0b4c5a6e1ac3ee304be4f249e348bf2cd8cceda415e5fd2 -frontend/components/BatchFilterDropdown.tsx,"React dropdown component for filtering and syncing prompts by batch, with compact and expanded views showing color indicators, module lists, and sync controls.","[""BatchFilterDropdown""]","[""react""]",fdf1629c5cf7fe742d278b549214f60c479fc7b3c8c9ea357db0c0596ac8c51d -frontend/components/BugModal.tsx,React modal component that lets users describe a bug in plain text and submit it to generate a test case.,"[""BugModal""]","[""react""]",cc20a690114c6aea7b700298ea8c8c700313c30bb0ff90e6a7af0824b35a3c02 -frontend/components/ChangeModal.tsx,"React modal component that collects a user's change request, sends it to an AI for prompt-file detection, and confirms the suggestion before proceeding.","[""ChangeModal""]","[""react"", ""./Icon""]",0ffb86438172c0b6586846d2b3a3260d676b3be0b0c4ba951172533f38db02d6 -frontend/components/CommandForm.tsx,"React component that dynamically renders a command configuration form with input fields, text areas, and optional file browser buttons.","[""CommandForm""]","[""react"", ""../types"", ""./InputField"", ""./TextAreaField""]",0e119032d176b29a6b555945191dd9d20c362c6008d89d4bb1926049da7e5f40 -frontend/components/CommandOutput.tsx,"React component that displays a real-time, streaming command output modal with WebSocket integration, job cancellation, status tracking, and auto-scrolling.","[""CommandOutput""]","[""react"", ""../api""]",1a191e67d34129750e47931e5e1794ac068000a76917072d0849015a78eb4e24 -frontend/components/CreatePromptModal.tsx,"React modal component that provides a form for creating new prompts with name, language, and description fields, including validation and duplicate detection.","[""CreatePromptModal""]","[""react""]",df363b52db32c1b53fa57eb707469c0a325e39705936fc02fb818e9beb8a9e87 -frontend/components/DependencyViewer.tsx,"React component that renders an interactive dependency graph of architecture modules using React Flow, supporting Dagre auto-layout, group nodes, focus mode, batch filtering, and edit-mode operations.","[""DependencyViewer"", ""computeGroupLayout"", ""getLayoutedElements"", ""buildEdgePath"", ""getCompactFontPx"", ""VpZoomSync""]","[""react"", ""reactflow"", ""dagre""]",93d3db54e7ed7c553ee14e3d34cd54b40f02835c97cf106e0487cc425ccceb64 -frontend/components/DeviceIndicator.tsx,A development-only React component that displays the current responsive breakpoint and screen width in the bottom-left corner of the viewport.,"[""DeviceIndicator""]","[""react""]",fc9e622fa0504e048d7280e391332ef4bb9e3671a8d2fc09c251ff9aa5eb48b0 -frontend/components/ErrorBoundary.tsx,Provides a React ErrorBoundary component that catches rendering errors and displays a styled fallback UI with error details and a option to clear local storage and reload.,"[""ErrorBoundary""]","[""react""]",ef725da5ece45c9660dcd049084b8e9dcf92f215864e643b1022aa5d081d6624 -frontend/components/ExecutionModeToggle.tsx,React toggle component that lets users switch between local and remote command execution modes with styled buttons and contextual info.,"[""ExecutionModeToggle""]","[""react""]",bf8453b63a2e92e42361b13d283f53d99d72a79d46db6ee59c3e06ab3a724eea -frontend/components/ExtractsPanel.tsx,"React panel component that displays, expands, and manages cached extracts with support for loading content on demand and pruning orphaned entries.","[""ExtractsPanel""]","[""react"", ""../api"", ""./Icon""]",ccd4987d786faedb62574d1ca31ab425432d13a1a68731bb8157b1b0fd846dd3 -frontend/components/FileBrowser.tsx,"React component that renders an interactive file browser tree with search, file-type filtering, directory selection mode, and optional modal display.","[""FileBrowser""]","[""react"", ""../api"", ""./Icon""]",fb2363de773f6fdb3fabbea2fcafabd4e0a7cdd04fb3bb2cc51d8b13734f24c9 -frontend/components/FilePickerInput.tsx,Provides a reusable React file picker input component that combines a text input with a browse button to open an integrated file/directory browser modal.,"[""FilePickerInput""]","[""react"", ""./FileBrowser"", ""./Icon""]",760e946a32c6610ac8df8fb52a59f2c7710078c330e2ce41ac97375d29c9c169 -frontend/components/GeneratedCommand.tsx,React component that displays a generated command string with copy-to-clipboard and optional execute functionality.,"[""GeneratedCommand""]","[""react"", ""./Icon"", ""./Tooltip""]",fbb0929c5ca878519d1fb0431943d691412ea1538c73df0c78f56be2e4f64c1b -frontend/components/GenerationProgressModal.tsx,"React modal component that displays prompt generation progress with a progress bar, current status, and a summary of success/failure results upon completion.","[""GenerationProgressModal""]","[""react""]",2c3ea0722b48686237858550f16d2e8b11a1cc02e62bbc7cc9b99993330defbb -frontend/components/GraphToolbar.tsx,"React toolbar component for a graph editor providing edit mode toggling, undo/redo, save/discard, module addition, group management, layout rearrangement, and prompt syncing actions.","[""GraphToolbar""]","[""react"", ""./Tooltip""]",3709b1a28bb142bf6e400203dbb1b379e974d7d86dcae362076bd558cbc2bfb4 -frontend/components/GroupEditModal.tsx,React modal component for creating or editing a named group of architecture modules with checkbox-based module selection.,"[""GroupEditModal""]","[""react""]",f0c3d289c7076597bdac584fea4e36d4d70d464cc7a44332ae7e660144d3862c -frontend/components/GroupNode.tsx,Defines a React Flow custom node component that renders an expandable/collapsible group of architecture modules with edit capabilities.,"[""GroupNode"", ""GroupNodeData"", ""GROUP_NODE_WIDTH"", ""GROUP_NODE_HEIGHT""]","[""react"", ""reactflow""]",38254b8f6f1c3b663e433f2171a336e9a68bab50f032d07a3b81f5b83a6042ab -frontend/components/GuidanceSidebar.tsx,"React sidebar component that displays PDD directive references, syntax examples, and best practices as an expandable prompting guide panel.","[""GuidanceSidebar""]","[""react"", ""../lib/pddDirectives"", ""./Icon""]",634d41b4d5b0bf234f1c416b5cae52464e7ce6ae80119c23f19d0e3ef95124bb -frontend/components/Header.tsx,"React header component that displays the app title, a tagline link, and navigation buttons for switching between ""Graph"" and ""Builder"" views.","[""Header""]","[""react"", ""./Tooltip""]",7190a23f842b1766a465cf1bd8660905568b8c7c8b787585aad03a89e7797295 -frontend/components/Icon.tsx,"Exports a collection of reusable React SVG icon components for UI elements like clipboard, check, chevron, spinner, folder, and settings icons.","[""ClipboardIcon"", ""CheckIcon"", ""ChevronUpIcon"", ""ChevronDownIcon"", ""SparklesIcon"", ""SplitIcon"", ""ConflictIcon"", ""LinkIcon"", ""SyncIcon"", ""DocumentArrowDownIcon"", ""LightBulbIcon"", ""SpinnerIcon"", ""BugAntIcon"", ""PlayIcon"", ""Squares2X2Icon""]","[""react""]",e13f13bc52d4f9e6fae13a04a501cd235b4398790db6baccc8010b6c680bee27 -frontend/components/InputField.tsx,"Defines a reusable React input field component with label, description, validation indicator, and optional file-browse button.","[""InputField""]","[""react"", ""./Icon""]",2cf6b1e97d334826b9ecb3ede034e8ba3d8b5d7da40cd95a6ca8de4277faa0b8 -frontend/components/JobCard.tsx,"React component that renders a job card displaying status, progress, cost, output preview, and action buttons for managing async, spawned, and remote jobs.","[""JobCard""]","[""react""]",6c15e39e5e26cf74aeb00faf0e2e2202d47f43dc67553211c93c348a8a9aa07c -frontend/components/JobDashboard.tsx,"React component that renders a collapsible, fixed-position dashboard panel displaying active and completed jobs with expandable inline output and batch operation tracking.","[""JobDashboard"", ""BatchOperation""]","[""react""]",d4c81e325f6a8f94ef837d55b7aee8368a33369548091e760c4bd0b235b8f953 -frontend/components/JobOutputPanel.tsx,"React component that displays detailed job output with streaming auto-scroll, progress bar, copy-to-clipboard, and cancel/close actions.","[""JobOutputPanel""]","[""react""]",be4d89283bd09cbe24066bd88c42a81310a9e1f56b083e6e3709f9552c6207c9 -frontend/components/ModelSelector.tsx,"React dropdown component that fetches available AI models from an API and lets users select one, displaying provider grouping, cost, context limit, and ELO rating.","[""ModelSelector""]","[""react"", ""../api""]",5b63a51a7bcb62d5cc01a397e7edccbfd4320c39f06b06e25af7bf43dd1729bd -frontend/components/ModelSliders.tsx,"React component providing a collapsible popover panel with sliders for adjusting model strength, reasoning time, and temperature parameters.","[""ModelSliders""]","[""react""]",cdcfbec342abace5acd7591cad66149c0465a921c2b6e1cf0b1ecb12e3f7745b -frontend/components/ModuleEditModal.tsx,"React modal component for editing architecture module properties including filename, filepath, description, priority, tags, group, interface type, and dependencies.","[""ModuleEditModal""]","[""react"", ""../api"", ""./FilePickerInput""]",5f242fcd2baffb9c45af300763d56310a4d1bd4c6dcb32472972c2c2040f7497 -frontend/components/ModuleNode.tsx,"Defines a React Flow graph node component for rendering architecture modules with prompt status indicators, edit/sync controls, and compact/full display modes.","[""ModuleNode"", ""ModuleNodeData""]","[""react"", ""reactflow"", ""../api"", ""./DependencyViewer"", ""./Tooltip""]",9b88d0fb31ec0d0d1d6855ee4258e112773075735fd92a86cb8f01636f35842b -frontend/components/ProjectSettings.tsx,"React component providing a project settings UI for editing .pddrc configuration contexts, output paths, language defaults, LLM extracts cache, and PDD Cloud authentication management.","[""ProjectSettings""]","[""react"", ""../api"", ""../types"", ""./Icon"", ""./Tooltip"", ""./FilePickerInput"", ""./ExtractsPanel""]",df80299e8a79775218690c4fc8063ecf99a5ef4e1394d940464f502c73b7db87 -frontend/components/PromptCodeDiffModal.tsx,A full-screen React modal component for visualizing bidirectional prompt-to-code alignment diffs and prompt version history with side-by-side comparison.,"[""PromptCodeDiffModal""]","[""react"", ""../api"", ""../types""]",31c28884c864c32646cc386748ad2097fa80fc3e93867b98658610ae8b7d88fb -frontend/components/PromptEditor.tsx,"React component providing a CodeMirror-based editor for loading, editing, and saving prompt files with change tracking and error handling.","[""PromptEditor""]","[""react"", ""@codemirror/state"", ""@codemirror/view"", ""@codemirror/commands"", ""@codemirror/lang-markdown"", ""@codemirror/theme-one-dark""]",45abcd755708554ffc532e60770500090206577304c21e8ea963d76ffac01097 -frontend/components/PromptMetricsBar.tsx,"React component that displays a toolbar with prompt token metrics, context usage visualization, view mode toggle, and action buttons for code/test/example/preview/diff panels.","[""PromptMetricsBar""]","[""react"", ""../api"", ""../lib/format""]",06564980552ea527a5eb6b00b5c7d32feb794bc6a197a688001530fb4e279c9b -frontend/components/PromptOrderModal.tsx,"React modal component that lets users select, reorder, and configure architecture modules for prompt generation with advanced global options.","[""PromptOrderModal""]","[""react"", ""../api"", ""../constants"", ""../types""]",508904f4c549f02fd5216a1b569a1d119812117c8870413936b491be72478dfa -frontend/components/PromptSelector.tsx,"React component that displays a searchable, filterable grid of prompt cards with support for creating, editing, syncing, and queuing prompts.","[""PromptSelector""]","[""react"", ""../api"", ""./CreatePromptModal"", ""./SyncOptionsModal""]",26a3d92bfd0a5208b7c728a388e668a2a2107326d2e53277990f40f3a65daa07 -frontend/components/PromptSpace.tsx,"A React component providing a full-featured prompt editing workspace with CodeMirror editor, command execution sidebar, side-by-side code/test/example panels, markdown preview, token analysis, and model configuration.","[""PromptSpace""]","[""react"", ""@codemirror/state"", ""@codemirror/view"", ""@codemirror/commands"", ""@codemirror/autocomplete"", ""@codemirror/lang-markdown"", ""@codemirror/lang-python"", ""@codemirror/lang-javascript"", ""@codemirror/lang-java"", ""@codemirror/theme-one-dark"", ""marked""]",2b6bfddf5b582cd24e7fdc2b38dde8471eaf17fc677d0c888df3902563ebffe7 -frontend/components/ReauthModal.tsx,"React modal component that handles GitHub device-flow re-authentication, including login initiation, code display, clipboard copy, and polling for authorization completion.","[""ReauthModal""]","[""react"", ""../api""]",e4a9e3e9e1326be5e56e2fd492d824d33df9b515d2e4d0317c0d2f9b54922d1f -frontend/components/RemoteSessionSelector.tsx,"React component that renders a dropdown selector for remote sessions with status indicators, debug info panel, and stale-session warnings.","[""RemoteSessionSelector""]","[""react""]",9b42f0456875847a7d7a47476a19f2540b4d68db504b63427db001ce51d595ca -frontend/components/SyncFromPromptModal.tsx,"React modal component that lets users sync architecture.json from prompt metadata tags, displaying pre-sync explanation, progress, and detailed per-module results with validation errors.","[""SyncFromPromptModal""]","[""react"", ""../api""]",c7cb753a020d122a5991233d1d153f285e3e3bdcc83bfc0b89bbc1c698aaf371 -frontend/components/SyncOptionsModal.tsx,Provides a React modal component for configuring sync command options (such as agentic mode) before executing a sync operation.,"[""SyncOptionsModal""]","[""react""]",8dedc339185726a344da7054e44f373e3eae69a4b256f9748d7869a918c98c4d -frontend/components/SyncStatusBadge.tsx,React component that displays a color-coded badge indicating the synchronization status between a prompt and its generated code.,"[""SyncStatusBadge""]","[""react"", ""../api""]",3791184d239610a1c1d1d679346c708626b15daedbee6d66798a804c6ecfed87 -frontend/components/Tabs.tsx,"React component that renders a horizontal tab bar for switching between command types, highlighting the currently active tab.","[""Tabs""]","[""react""]",838765f3bb9077dfaeab7f7c021a5e8140415ee5a74d6fb5c50ed1a82a33d551 -frontend/components/TaskQueueControls.tsx,"React component that renders a control bar for a task queue, providing execution mode toggling, start/pause/resume actions, progress display, and clear buttons.","[""TaskQueueControls""]","[""react""]",e48416a9a55dbc97816ef56c81e083cc86208bf0a0e5c0a5517ca717b9d35beb -frontend/components/TaskQueueItem.tsx,"React component that renders a single task queue item with status badges, duration, drag-and-drop support, and action buttons for run, skip, retry, and remove.","[""TaskQueueItem""]","[""react""]",a651d053438f0553b18968f4421aae4877d421c6843146df8adbf276d87a2636 -frontend/components/TaskQueuePanel.tsx,"A draggable, collapsible React panel component that displays a task queue with execution controls, drag-to-reorder support, and persistent panel positioning.","[""TaskQueuePanel""]","[""react""]",2db63f1a10010ee8bd58f765c50312da7b2de39316a6344d70c716e36c56dabf -frontend/components/TextAreaField.tsx,"Defines a reusable React textarea form field component with label, placeholder, description, and optional required indicator.","[""TextAreaField""]","[""react""]",8815b4343c4df6c5815f3f4ef23761725c42a539b6ef8abd3cd0d4ccaac473eb -frontend/components/Toast.tsx,"Provides a toast notification system with auto-dismiss, success/error/info variants, browser notification support, and a React context-based provider and hook.","[""ToastType"", ""ToastMessage"", ""ToastProvider"", ""useToast""]","[""react""]",86e96cb2c88a1e85b2c9b4b487775e052750873ba80087d765ac61b506b442ad -frontend/components/Tooltip.tsx,Defines a reusable React Tooltip component that displays hover-triggered tooltip content positioned below its children.,"[""Tooltip""]","[""react""]",52e48eb3ac7900db5d27115b9ec34ee37d84471f6ca086ad5fccb8bb2319e336 -frontend/constants.ts,"Defines CLI command configurations, global options with defaults, and helper utilities for a PDD (Prompt-Driven Development) tool's frontend interface.","[""GLOBAL_DEFAULTS"", ""GLOBAL_OPTIONS"", ""COMMANDS"", ""getCommandByBackendName"", ""SUPPORTED_LANGUAGES"", ""getPromptTemplate""]","[""./types""]",1a549aec87b4ae4537f8df424bc98ff1048df5512756b61d47d58d2f9685c061 -frontend/data/mockPrd.ts,"Exports a mock Product Requirements Document (PRD) string for a Prompt Driven Development IDE, used as test or demo data.","[""mockPrd""]",[],8bf6a64f32da3c70cf9aeb47e2c7e908560b53ef5722bc03406f0ef8654be671 -frontend/data/mockPrompts.ts,"Defines an array of mock prompt fixtures containing sample React/TypeScript component definitions, configurations, and test cases for use in testing.","[""mockPrompts""]",[],d4b93c959e47e190a95c8a2cf1aaa78b5e74e7a480391c18c75a08415bbf177c -frontend/hooks/useArchitectureHistory.ts,"React hook providing undo/redo history management for architecture module state, including CRUD operations on modules, dependencies, and positions.","[""useArchitectureHistory""]","[""react""]",e95a989334e61112f3e3fc0ae23be0b7130e6c68d9953f9e680a8789eb09f854 -frontend/hooks/useAudioNotification.ts,"React hook that manages audio notification sounds for job/task completion using the Web Audio API, with localStorage-persisted user preferences.","[""useAudioNotification"", ""UseAudioNotificationReturn""]","[""react""]",1699c71d34c4b3a42cb601835478da69c1c35856444143148427b63471f66aba -frontend/hooks/useJobs.ts,"React custom hook that manages concurrent job execution state, WebSocket streaming, polling for local/remote job status, and job lifecycle actions like submit, cancel, and cleanup.","[""useJobs"", ""JobStatus"", ""JobProgress"", ""JobInfo"", ""UseJobsOptions""]","[""react""]",6af56aeb30a34cce916fb4f0e91fd9038fa0d3a6f6704368898a31c55ad04566 -frontend/hooks/useTaskQueue.ts,"Custom React hook for managing a sequential task queue with support for auto-run and manual execution modes, localStorage persistence, and lifecycle callbacks.","[""useTaskQueue"", ""TaskStatus"", ""ExecutionMode"", ""TaskQueueItem"", ""UseTaskQueueOptions""]","[""react""]",90decc5007998e42b7c26908c7212d3eb1936362f2fd47ff8470f9c8dd19bb38 -frontend/index.html,"Entry-point HTML file for a ""Prompt Driven Development"" single-page application, configuring Tailwind CSS theming, custom animations, dark-mode styles, and module import maps.",[],[],83b3f9c62e650557b691323aaceef124d05aeb5e9b2006a7b7cecb5fc9729580 -frontend/index.tsx,"Entry point that mounts the React application into the DOM, wrapped in StrictMode and a ToastProvider context.",[],"[""react"", ""react-dom"", ""./App"", ""./components/Toast""]",29f7a76e813c782fc068fa8a4ab32b4173f15c45df40636ccfd7c369c87938e0 -frontend/lib/batchUtils.ts,"Computes connected components (batches) from an architecture dependency graph using Union-Find, grouping interdependent modules for sequential syncing.","[""Batch"", ""computeBatches"", ""getBatchForModule"", ""filterModulesByBatch""]",[],9582e5d41c188845d56bbc957f054647fee56a78869544c0a7d6ec3340bf7ee6 -frontend/lib/commandBuilder.ts,"Provides centralized functions for building PDD CLI command arguments, options, and display strings used by UI components.","[""cleanOptions"", ""buildCommandArgs"", ""buildDisplayCommand"", ""buildCommandRequest""]","[""../types"", ""../api"", ""../constants""]",d461731ee1d65db39fd67e64f1a9967dbc6a843bed3e8ad80498730e3371d97b -frontend/lib/format.ts,Provides a utility function for formatting numeric costs as dollar-denominated strings with variable decimal precision.,"[""formatCost""]",[],08dd5f8b9a9cb4f8a5195251a1f9831f4872a56b32f8dd7c3ff2d69614fee9fa -frontend/lib/includeAnalyzer.ts,"Detects and analyzes include-type XML tags (include, include-many, shell, web) at the cursor position in a CodeMirror editor.","[""IncludeTagInfo"", ""findIncludeAtCursor"", ""getIncludeTagLine"", ""parseIncludeManyPaths""]","[""@codemirror/state""]",e5699fa72eb2eb1373f950524facc097b984e9578dc066bce044d40b57765f61 -frontend/lib/modelResolver.ts,Defines a hardcoded catalog of LLM models with metadata and a function to select a model based on a strength slider value mapped to ELO rankings.,"[""ALL_MODELS"", ""resolveModelForStrength""]","[""../api""]",c825938bff2b3e1d8d62258e0e481857c5d63707eee21ae258c794c977108884 -frontend/lib/pddAutocomplete.ts,"Provides a CodeMirror 6 autocomplete extension for PDD directives, offering filtered suggestions with styled tooltips when users type angle brackets.","[""pddAutocompleteExtension""]","[""@codemirror/autocomplete"", ""@codemirror/view""]",24027210f5a52e550961c215bbeb54af78c5153de67bcf992a2c71a5d99267a6 -frontend/lib/pddDirectives.ts,"Defines PDD (Prompt-Driven Development) directive metadata, best practices, and category labels for use in autocomplete and a guidance sidebar.","[""PDDDirective"", ""PDD_DIRECTIVES"", ""BEST_PRACTICES"", ""CATEGORY_LABELS""]",[],84343eaacb567b11a9e1a4b9c8daeef8ec3ea15bc6f44d5dbce8880ce1f96f13 -frontend/metadata.json,"JSON manifest file defining metadata for a Prompt Driven Development IDE application, including its name and description.",[],[],236fa979e03b3c15ba809c754b591c44510c5509484a0e69f60b47bdfabff806 -frontend/package-lock.json,NPM lockfile (package-lock.json) pinning exact dependency versions for a React-based prompt-driven development IDE project.,[],[],7c6ad02d58863d57d952e5f9f7c7084ac2f2de24aa71ccdb3581cd61814a4a83 -frontend/package.json,"Package configuration for a prompt-driven development IDE built with React, CodeMirror, ReactFlow, and Google GenAI, using Vite as the build tool.",[],[],56fe6f5bf76773df49b304f355ced0c42610df40192fd65d151205f79210b9ca -frontend/tests/compact-font-direct-zoom.test.tsx,Tests that ModuleNode's compact-mode label computes its font-size directly from the viewport zoom value rather than relying on a CSS custom property.,[],"[""react"", ""vitest"", ""@testing-library/react"", ""reactflow""]",1144cff680884affe63fe945afea6590d5c4266d984569f8013d847919370328 -frontend/tests/compact-font-size.test.ts,Unit tests verifying that getCompactFontPx computes an inverse-zoom font size so text appears at a constant 14px on screen.,[],"[""vitest"", ""../components/DependencyViewer""]",236c70fc5074331c04bbaed9b667df82d27fcf9bc3cdff9c3a4213b442e38184 -frontend/tests/group-layout.test.ts,Unit tests verifying the computeGroupLayout helper's grid sizing and child positioning logic for the DependencyViewer component.,[],"[""vitest"", ""reactflow""]",482184cd15b2d28d4493eb85487789d052b5487830b710d4f0e53da4cae8e075 -frontend/tests/group-subflow-nodes.test.tsx,"Unit tests verifying that the DependencyViewer component correctly renders expanded/collapsed group nodes, child node positioning, and parent-child relationships in a ReactFlow graph.",[],"[""@testing-library/react"", ""vitest"", ""react""]",afe6ddbd0d1669e2e947e520d1dbf0c8b0427aab138a45a8b277e6fd8a4a4642 -frontend/tests/module-needs-sync.test.tsx,"Unit tests for the `moduleNeedsSync` function, verifying it correctly detects when module outputs (code, test, example) are missing and require synchronization.",[],"[""vitest""]",e5d432057fc383e8f86dc95898b90762637259864fc8d4fecdf8c73535a5a122 -frontend/tests/rearrange-discard.test.tsx,Tests that the ArchitectureView component correctly increments rearrangeVersion when the Discard button is clicked after a Re-arrange action.,[],"[""@testing-library/react"", ""vitest"", ""react""]",9df8e0282d7b33aa365e99e56d76516e291c2dd50d5d0189f4674619059422e4 -frontend/tests/rearrange-positions.test.tsx,Tests that clicking the re-arrange button in ArchitectureView increments the rearrangeVersion prop passed to DependencyViewer.,[],"[""@testing-library/react"", ""vitest"", ""react""]",bcf2aa977889df91638f59fb70cee2b8924ae4611a52320a7b629e4b81eabea9 -frontend/tests/setup.ts,Test setup file that configures jest-dom custom matchers for use in testing-library-based tests.,[],"[""@testing-library/jest-dom""]",a46d66851af2c056e805fdd574bf5ec3adb1181c43c5e41f0a1c592e338afe64 -frontend/tests/sync-completed-refresh.test.tsx,"Tests that the ArchitectureView component refreshes prompt data when a sync-completed counter increments, and verifies the counter logic only increments for successful SYNC tasks.",[],"[""@testing-library/react"", ""vitest"", ""react""]",3fde1ff581bc5358d36892bf1a4d2116ee984c0b3825583d0f280639cbbf10f6 -frontend/tests/vp-zoom-sync.test.tsx,Unit tests verifying that the VpZoomSync component keeps a CSS custom property (--vp-zoom) synchronized with the ReactFlow viewport zoom.,[],"[""react"", ""vitest"", ""@testing-library/react"", ""reactflow"", ""../components/DependencyViewer""]",a7ba5d078b2ca1d14950bec01c6a73503df66f57ea6202add574d409e06b4803 -frontend/tests/waypoint-edge.test.ts,"Unit tests for `getLayoutedElements` and `buildEdgePath` functions exported from the DependencyViewer component, verifying waypoint-based edge routing and smooth bezier curve path generation.",[],"[""vitest""]",c0679ec2d76a16ad8d090433525183f8cb5b3018cc7a9b4d009321955da68088 -frontend/tsconfig.json,"TypeScript compiler configuration file defining ES2022 target, React JSX support, bundler module resolution, and path aliases.",[],[],43d5f50b8182e186dc8b7e091cbdae4477888002dc0500b1765dc1f453378637 -frontend/types.ts,"Defines TypeScript enums, interfaces, and types for PDD CLI command configuration, project architecture modules, and `.pddrc` configuration.","[""CommandType"", ""CommandOption"", ""GlobalOption"", ""GlobalDefaults"", ""CommandConfig"", ""PromptInfo"", ""DevUnit"", ""MockPrompt"", ""ArchitectureInterface"", ""ArchitectureModule"", ""ProjectArchitecture"", ""PddrcContextDefaults"", ""PddrcContext"", ""PddrcConfig""]",[],f6d60506e61cfa3c565477f0af7f4598108acee5326daa3487ca094fb4fd9972 -frontend/vite.config.ts,"Vite configuration file that sets up a React dev server with API/WebSocket proxying, path aliases, environment variable injection, and Vitest testing settings.",[],"[""path"", ""vite"", ""@vitejs/plugin-react""]",c92ee737487ccaf5c4bb5083119696f6bf077ef0626a01b7aa1be92107e1b160 -generate_model_catalog.py,"Generates a curated LLM model catalog CSV from LiteLLM's registry, filtering by quality (ELO scores), deduplicating variants, normalizing pricing, and enriching with provider metadata.","[""ELO_CUTOFF"", ""ELO_SCORES"", ""PRICE_OVERRIDES"", ""PROVIDERS"", ""CSV_FIELDNAMES"", ""build_rows"", ""main""]","[""argparse"", ""csv"", ""re"", ""sys"", ""collections"", ""datetime"", ""pathlib"", ""typing"", ""litellm""]",2937cc14412704887dafebd386c2578ec1e86d17562fe62eb95557ac8e6f4ac4 -generate_output_paths.py,"Resolves output file paths for PDD commands by prioritizing user-specified paths, .pddrc context config, environment variables, and default naming conventions.","[""PathResolutionMode"", ""COMMAND_OUTPUT_KEYS"", ""DEFAULT_FILENAMES"", ""ENV_VAR_MAP"", ""CONTEXT_CONFIG_MAP"", ""EXAMPLES_DIR"", ""generate_output_paths""]","[""os"", ""logging"", ""typing""]",76cbf3665903fe3f0e59483508bb27c8b44c533d52849c6bbb0ec0edcedaba85 -generate_test.py,"Generates unit tests from source code or example files by orchestrating LLM invocation, incomplete-output continuation, postprocessing, and sys.path preamble injection.","[""generate_test""]","[""re"", ""typing"", ""rich"", ""pdd""]",c34164ec5abded5fbc0e600f3ad007fc6175a29d0662dded3d57edbae9e3fb17 -get_comment.py,"Looks up the comment syntax for a given programming language from a CSV data file, returning ""del"" as a default fallback.","[""get_comment""]","[""csv"", ""pdd""]",53e70fe3e4f7ba637f7c754d95a751e068754eb06d265b1e95636349c118a883 -get_extension.py,Provides a function to look up file extensions for programming languages from a CSV data file.,"[""get_extension""]","[""pandas"", ""pdd""]",4f9ab4599944c8faf14fb060f83a0b6745d1d096b357de34b21c3b34a84480d7 -get_jwt_token.py,"Implements GitHub Device Flow OAuth authentication with Firebase token exchange, JWT caching, and secure keyring-based refresh token storage for CLI applications.","[""get_jwt_token"", ""DeviceFlow"", ""FirebaseAuthenticator"", ""AuthError"", ""NetworkError"", ""TokenError"", ""UserCancelledError"", ""RateLimitError"", ""KEYRING_AVAILABLE"", ""JWT_CACHE_FILE""]","[""asyncio"", ""base64"", ""json"", ""logging"", ""os"", ""subprocess"", ""sys"", ""time"", ""webbrowser"", ""pathlib"", ""typing"", ""keyring"", ""requests""]",6fc201700ee5f6bddff34eb6d6595276191e876cf084f8491dc2d14ab7e8325f -get_language.py,Maps file extensions to programming language names by looking up entries in a CSV data file.,"[""get_language""]","[""csv"", ""pdd""]",65004add40c93365c1cbd705a6ef24d5bda6f7016c3a67749b25e61122c50ce7 -get_run_command.py,Provides functions to look up programming language run commands from a CSV file based on file extension.,"[""get_run_command"", ""get_run_command_for_file""]","[""os"", ""csv"", ""pdd""]",5eec778a234abc4ace4166d71a68f6e49f1b81491962867de81f61be882a5319 -get_test_command.py,"Resolves the appropriate language-specific test command for a given test file using CSV lookup, smart detection, or agentic fallback.","[""get_test_command_for_file""]","[""pathlib"", ""typing"", ""csv""]",1b8e0d7bb6d6d0f65e09b05a6f65d2ef7a254aec43a2cd56a2968f172f52531d -git_update.py,"Manages a git-based prompt update workflow that reads modified code, restores the prior committed version, updates the prompt via agentic or legacy paths, and restores the modified code.","[""git_update""]","[""os"", ""typing"", ""rich"", ""git""]",ca810925f80b85596c0f1f84b4a7c052ff19a8530d24ed6989730467df2a3f17 -include_query_extractor.py,"Provides LLM-based semantic extraction of relevant content from source files, with persistent SHA-256-keyed disk caching under `.pdd/extracts/`.","[""IncludeQueryExtractor"", ""compute_cache_key"", ""EXTRACTION_STRENGTH""]","[""hashlib"", ""json"", ""os"", ""tempfile"", ""time"", ""pathlib"", ""pdd"", ""rich""]",5a9a5391f7878f3390da4553adc8729e2b2bdd93e006f8a42a05225f0d4435fd -increase_tests.py,"Generates additional unit tests to increase code coverage by invoking an LLM with existing tests, coverage reports, and source code.","[""increase_tests""]","[""typing"", ""rich""]",ebc70cf5dd42a5a2cb9b6212169270dfdc5b463b1fc31c12d3ac80c11a140879 -incremental_code_generator.py,Analyzes prompt diffs and either incrementally patches existing generated code or recommends full regeneration using LLM-based diff analysis and code patching.,"[""incremental_code_generator"", ""DiffAnalysis"", ""CodePatchResult""]","[""typing"", ""pydantic"", ""rich""]",4686890dabd1d00e726d773c3efa9eec7c30873f601db3ff9261905e7dd7bde4 -insert_includes.py,"Determines needed dependencies for a prompt using auto_include and an LLM, then inserts the relevant include blocks into the prompt text.","[""InsertIncludesOutput"", ""insert_includes"", ""main""]","[""os"", ""typing"", ""pathlib"", ""rich"", ""pydantic"", ""re""]",7aa8b935bc75ddb7e4571d4d8ff5cf74f48cee478a89a9e8b348fe633d0804d2 -install_completion.py,"Provides shell detection, completion script installation, and path resolution utilities for the PDD CLI tool.","[""get_local_pdd_path"", ""get_shell_rc_path"", ""get_current_shell"", ""get_completion_script_extension"", ""install_completion""]","[""os"", ""sys"", ""importlib"", ""typing"", ""click"", ""rich""]",6cb309b8c3810efb049c3014a6088f7ac3511a163f22f06fcfc66f99b4f33f8a -llm_invoke.py,"Provides LLM invocation with automatic model selection, API key management, structured output parsing, caching, cloud/local fallback, and reasoning/thinking support via LiteLLM.","[""llm_invoke"", ""SchemaValidationError"", ""CloudFallbackError"", ""CloudInvocationError"", ""InsufficientCreditsError"", ""setup_file_logging"", ""set_verbose_logging"", ""set_quiet_logging"", ""PROJECT_ROOT"", ""ENV_PATH"", ""LLM_MODEL_CSV_PATH"", ""DEFAULT_BASE_MODEL""]","[""copy"", ""os"", ""pandas"", ""litellm"", ""logging"", ""importlib"", ""json"", ""dotenv"", ""pathlib"", ""typing"", ""pydantic"", ""openai"", ""warnings"", ""time"", ""re"", ""pdd""]",772957fe64d05fb4eca172098fc0052c277c7c3ac47a06f9dc86a0eb6b2adfa1 -load_prompt_template.py,Loads prompt template files by name from configurable candidate directories using a path resolver.,"[""print_formatted"", ""load_prompt_template""]","[""pathlib"", ""typing"", ""rich"", ""pdd""]",0380b08510aee06d4e3e2b192be66475a7e3a93586b21c6fc4f9be7338bb025b -logo_animation.py,"Implements a threaded terminal logo animation that forms ASCII art from particles, holds it, then expands particles to a box perimeter using Rich for rendering.","[""AnimatedParticle"", ""start_logo_animation"", ""stop_logo_animation"", ""run_logo_animation_inline""]","[""time"", ""threading"", ""math"", ""typing"", ""dataclasses"", ""rich""]",9fa1c95b3b85cded9cb00016dbecdbc5f8ef585611238848770541b4704e923e -mcp_config.json,JSON configuration file defining an MCP text editor server that runs via npx with stdio transport.,[],[],0f772d58795286f96d6ed1f7ef36d86fab26544d15f34fe51a30ca0c8aac4811 -model_tester.py,"Provides an interactive CLI tool for loading, diagnosing, and testing LLM model configurations from a user CSV, displaying results in a rich table.","[""test_model_interactive""]","[""os"", ""sys"", ""threading"", ""time"", ""pathlib"", ""typing"", ""pandas"", ""rich"", ""pdd"", ""litellm"", ""dotenv""]",2a1caa5e935c1d30dfee7540eba4880ff8dce6b45a9ac10eb26a64c48e32cfc3 -one_session_sync.py,"Orchestrates a single agentic session to run example generation, crash-fix, verification, test generation, and test-fix steps for PDD module syncing.","[""build_one_session_prompt"", ""run_one_session_sync""]","[""logging"", ""threading"", ""time"", ""pathlib"", ""typing"", ""rich""]",50bdb0e40cf0e01ba5441e0a4fe0509f5b6c1e9ce531233ba85509a0f91841e2 -operation_log.py,"Provides metadata path management, operation logging, fingerprint persistence, and run report handling for a prompt-driven development (PDD) sync system.","[""PDD_DIR"", ""META_DIR"", ""ensure_meta_dir"", ""get_log_path"", ""get_fingerprint_path"", ""get_run_report_path"", ""infer_module_identity"", ""load_operation_log"", ""append_log_entry"", ""create_log_entry"", ""create_manual_log_entry"", ""update_log_entry"", ""log_event"", ""save_fingerprint"", ""save_run_report"", ""clear_run_report"", ""log_operation""]","[""functools"", ""json"", ""os"", ""re"", ""time"", ""datetime"", ""pathlib"", ""typing"", ""rich""]",fdfa78636b215b118cb71f6b8945b9573857244b01226731fc7bdb08f3e092db -path_resolution.py,"Provides a configurable PathResolver for locating includes, prompt templates, data files, and project roots across multiple directory search paths.","[""PathResolver"", ""get_default_resolver"", ""IncludeProfile"", ""PromptProfile"", ""DataProfile"", ""ProjectRootProfile""]","[""__future__"", ""dataclasses"", ""os"", ""pathlib"", ""typing""]",4c513395cd96ff1fe3c01c01e6e4c2de3617332f6ff87c8391083f0c26d41d75 -pdd_completion.fish,"Provides Fish shell tab-completion definitions for the PDD CLI tool, covering global options, subcommands, and command-specific flags and file-type completions.",[],[],a5f132056cc5a7799ab42fd25b60530cd6812726d8294122dd8a4f13e0fa00a5 -pdd_completion.sh,"Provides Bash tab-completion for the PDD CLI tool, supporting all commands, subcommand-specific options, and filename completion.","[""_pdd"", ""_complete_files""]",[],c60c7e83a69e082ae587a96878bc893798c2b17d79617ad6c93d54e3df69dc2d -pdd_completion.zsh,"ZSH shell completion script providing tab-completion for the `pdd` (Prompt-Driven Development) CLI's subcommands, options, and arguments.","[""_pdd""]",[],57df95f23ab0de8b2395734ebe360c25fd04efbd7efc441ef826e8a35c733ef6 -pddrc_initializer.py,"Provides detection, creation, and auto-generation of `.pddrc` project configuration files with language-aware defaults and directory-scanning context inference.","[""PYTHON_MARKERS"", ""TYPESCRIPT_MARKERS"", ""GO_MARKERS"", ""RUST_MARKERS"", ""LANGUAGE_DEFAULTS"", ""STANDARD_DEFAULTS"", ""PDDRC_FILENAME"", ""offer_pddrc_init"", ""infer_contexts_from_scan"", ""ensure_pddrc_for_scan""]","[""os"", ""re"", ""collections"", ""pathlib"", ""typing"", ""rich""]",108185fcb6513b50582f3c217ac95562bb12a5205ca092cf85d6cc6512bfbc66 -pin_example_hack.py,"Orchestrates the PDD sync workflow by coordinating code generation, testing, crash fixing, and verification operations with parallel TUI animation, budget tracking, atomic state updates, and cycle detection.","[""sync_orchestration"", ""AtomicStateUpdate"", ""PendingStateUpdate"", ""load_sync_log"", ""create_sync_log_entry"", ""update_sync_log_entry"", ""append_sync_log"", ""log_sync_event"", ""save_run_report"", ""MAX_CONSECUTIVE_TESTS"", ""MAX_TEST_EXTEND_ATTEMPTS"", ""MAX_CONSECUTIVE_CRASHES""]","[""threading"", ""time"", ""json"", ""datetime"", ""subprocess"", ""re"", ""os"", ""pathlib"", ""typing"", ""dataclasses"", ""tempfile"", ""sys"", ""click"", ""logging""]",8cee5904fe3807689ff6d6e8e55761f226eb94174c25c7af4bd521b5b32693f9 -postprocess.py,Extracts code blocks from LLM text output using either simple regex parsing or an LLM-based structured extraction approach.,"[""ExtractedCode"", ""postprocess"", ""postprocess_0""]","[""re"", ""typing"", ""rich"", ""pydantic""]",8d2e931b415a1f8f2a49523b32822c77ab97b1cdd79a6ce0bb6f403f69369893 -postprocess_0.py,Processes LLM output by identifying the largest code section for a given language and commenting out all lines outside it.,"[""postprocess_0""]","["".get_comment"", "".comment_line"", "".find_section""]",396d7b1b20852d812b0b25a1dad2f8b32ba1a37ab6c857f6c327a443804b74f3 -preprocess.py,"Preprocesses prompt templates by resolving file includes (backtick, XML, include-many), executing shell commands, scraping web content, and escaping curly braces for PromptTemplate compatibility.","[""preprocess"", ""process_backtick_includes"", ""process_xml_tags"", ""process_include_tags"", ""process_include_many_tags"", ""process_pdd_tags"", ""process_shell_tags"", ""process_web_tags"", ""double_curly"", ""get_file_path""]","[""os"", ""re"", ""base64"", ""subprocess"", ""typing"", ""traceback"", ""pathlib"", ""rich"", ""pdd"", ""PIL"", ""pillow_heif"", ""firecrawl"", ""io"", ""warnings"", ""concurrent""]",1ad6645d671d2e82120977e552f61e279ca5b94ff91cc7d900d8a18a0f976b26 -preprocess_main.py,"CLI wrapper that orchestrates prompt preprocessing, including optional XML tagging, PDD metadata injection, and output file saving.","[""preprocess_main""]","[""csv"", ""sys"", ""pathlib"", ""typing"", ""click"", ""rich""]",6a95bf2ffb3889a23f530555f745619427b83ef2438a377ce2473b1ebc79b656 -process_csv_change.py,"Processes a CSV file of prompt-change instructions, resolves prompt and code file paths, and applies LLM-driven modifications with budget tracking.","[""resolve_prompt_path"", ""process_csv_change""]","[""csv"", ""os"", ""typing"", ""rich""]",3d2cba9a8d295658d32d8280e5052e0cbdbc23a4a64ca4b147af18b66e72d251 -provider_manager.py,"Manages LLM provider configuration including CSV-based model registry, shell-aware API key storage, and interactive setup/removal of providers and credentials.","[""CSV_FIELDNAMES"", ""COMPLEX_AUTH_PROVIDERS"", ""parse_api_key_vars"", ""is_multi_credential"", ""add_provider_from_registry"", ""add_custom_provider"", ""remove_models_by_provider"", ""remove_individual_models"", ""console""]","[""csv"", ""io"", ""os"", ""re"", ""shlex"", ""shutil"", ""tempfile"", ""datetime"", ""pathlib"", ""typing"", ""rich""]",789a8216ee2fd4271e0f23ae61d7829711a260c5852cf2723685a264ceb59b21 -pytest_output.py,"Runs pytest on specified test files via subprocess, captures and parses the output (pass/fail/error/warning counts), and optionally saves results as JSON.","[""run_pytest_and_capture_output"", ""save_output_to_json"", ""extract_failing_files_from_output"", ""TestResultCollector"", ""main""]","[""argparse"", ""json"", ""io"", ""re"", ""sys"", ""pytest"", ""subprocess"", ""pathlib"", ""rich"", ""os""]",b1ef53e8e80c7a08ab9e24c8121e5a11e44317257be2af9a704c5283fa247fba -python_env_detector.py,"Detects the host shell's Python environment (conda, venv, poetry, pipenv) and resolves the appropriate Python executable for subprocess calls.","[""detect_host_python_executable"", ""get_environment_info"", ""is_in_virtual_environment"", ""get_environment_type""]","[""os"", ""sys"", ""shutil"", ""pathlib"", ""typing""]",cbe4044a83cd88a683f36d6ecfca245fef6a03ea2abc7f5c407636fccc051f35 -remote_session.py,"Manages remote PDD Connect sessions via cloud APIs, handling registration, heartbeats, command polling/execution, cancellation, and JWT token refresh.","[""RemoteSessionManager"", ""RemoteSessionError"", ""SessionInfo"", ""CommandInfo"", ""get_active_session_manager"", ""set_active_session_manager"", ""console""]","[""ast"", ""asyncio"", ""datetime"", ""platform"", ""socket"", ""sys"", ""dataclasses"", ""pathlib"", ""typing"", ""httpx"", ""rich""]",163292e0200246767f685e9478c8ab4531c06ba471be4ceb9a6ee8065b1cfc12 -render_mermaid.py,Renders an architecture JSON file as a self-contained interactive HTML page with a color-coded Mermaid flowchart diagram and hover tooltips.,"[""generate_mermaid_code"", ""generate_html"", ""write_pretty_architecture_json"", ""INDENT"", ""LEVELS""]","[""json"", ""sys"", ""html"", ""pathlib""]",9ef14efd6979329608885df23294e817c78f7ab1ecaf773d595ef8e6ef98dd7f -server/__init__.py,"Package init that exposes the PDD server's public API, including app creation, job management, command execution, security, and websocket components.","[""create_app"", ""run_server"", ""execute_pdd_command"", ""Job"", ""JobManager"", ""ServerConfig"", ""ServerStatus"", ""ConnectionManager"", ""PathValidator"", ""SecurityError"", ""DEFAULT_HOST"", ""DEFAULT_PORT"", ""API_VERSION""]","[""__future__""]",8e83c199ca1766c89385734e0a1e33347a92f893f4032ee09a206ed3afc744bb -server/app.py,"Defines the FastAPI application factory, global application state, exception handlers, lifespan management, and server runner for a Prompt Driven Development (PDD) local REST server.","[""AppState"", ""create_app"", ""run_server"", ""get_app_state"", ""get_path_validator"", ""get_job_manager"", ""get_connection_manager"", ""get_server_port""]","[""asyncio"", ""contextlib"", ""datetime"", ""pathlib"", ""typing"", ""uvicorn"", ""fastapi"", ""rich""]",28394e19c9174f44cda9cf37b93229d74012bd2e61c49e0d03e665796be94279 -server/click_executor.py,"Provides programmatic execution of Click CLI commands with isolated contexts, output capture, real-time streaming, and a lazy-loading command registry for PDD server.","[""CapturedOutput"", ""StreamingWriter"", ""OutputCapture"", ""create_isolated_context"", ""ClickCommandExecutor"", ""get_pdd_command"", ""INTEGER_OPTIONS"", ""FLOAT_OPTIONS"", ""BOOLEAN_OPTIONS""]","[""io"", ""os"", ""sys"", ""dataclasses"", ""typing"", ""unittest"", ""click""]",b8e1e120d050f0545f2c3979c94390b07431a30af391bfd30bc052770f11edda -server/executor.py,"Provides utilities for programmatically executing Click CLI commands with isolated contexts, output capture, real-time streaming, and error handling.","[""CapturedOutput"", ""StreamingWriter"", ""OutputCapture"", ""create_isolated_context"", ""ClickCommandExecutor"", ""get_pdd_command"", ""execute_pdd_command""]","[""io"", ""sys"", ""dataclasses"", ""typing"", ""unittest"", ""click"", ""rich""]",249504fc9a00609062d2f846af665225b051f455115dee0af626324a3a7b99a9 -server/jobs.py,"Manages async job queuing, execution, cancellation, and lifecycle tracking for PDD CLI commands run as subprocesses with real-time output streaming.","[""JOB_TIMEOUT"", ""GLOBAL_OPTIONS"", ""POSITIONAL_ARGS"", ""MANUAL_MODE_FILE_KEYS"", ""Job"", ""JobCallbacks"", ""JobManager"", ""_build_subprocess_command_args"", ""_find_pdd_executable""]","[""asyncio"", ""logging"", ""os"", ""signal"", ""subprocess"", ""sys"", ""threading"", ""time"", ""concurrent"", ""dataclasses"", ""datetime"", ""pathlib"", ""typing"", ""uuid"", ""rich""]",9459f35b583e92ff7de783a42405e30583c3fe35ca14e41932212fba2a3f6fdc -server/models.py,"Defines Pydantic v2 models for a PDD Server REST API covering file operations, command/job management, WebSocket messaging, and server configuration.","[""FileMetadata"", ""FileTreeNode"", ""FileContent"", ""WriteFileRequest"", ""WriteResult"", ""CommandRequest"", ""JobHandle"", ""JobStatus"", ""JobResult"", ""WSMessage"", ""StdoutMessage"", ""StderrMessage"", ""ProgressMessage"", ""InputRequestMessage"", ""CompleteMessage""]","[""datetime"", ""enum"", ""typing"", ""pydantic""]",a17b2f09033b2fe7063f484a2efb59184f7cb2fba822ee25605a93bbe5905941 -server/routes/__init__.py,"Package init that aggregates and re-exports API router modules for architecture, auth, config, extracts, files, commands, prompts, and websocket endpoints.","[""architecture"", ""auth"", ""config"", ""extracts"", ""files"", ""commands"", ""prompts"", ""architecture_router"", ""auth_router"", ""config_router"", ""extracts_router"", ""files_router"", ""commands_router"", ""websocket_router"", ""prompts_router""]","[""__future__""]",be18cbdee75cb9afd377e404b189be8f3e5a578d638af87a5edf137498a4aaa1 -server/routes/architecture.py,"Defines FastAPI REST endpoints for validating architecture module dependencies, syncing architecture from prompt metadata, generating PDD tags, generating architecture from GitHub issues, and agentic graph layout rearrangement.","[""router"", ""ArchitectureModule"", ""ValidationError"", ""ValidationWarning"", ""ValidateArchitectureRequest"", ""ValidationResult"", ""SyncRequest"", ""SyncResult"", ""GenerateTagsRequest"", ""GenerateTagsResult"", ""GenerateFromIssueRequest"", ""GenerateFromIssueResult"", ""RearrangeRequest"", ""RearrangeResult""]","[""asyncio"", ""re"", ""json"", ""concurrent"", ""pathlib"", ""typing"", ""fastapi"", ""pydantic"", ""pdd""]",86b07e88dd7aeeb6683047cfc8b57b78ac480ada6e617f06b847c9f0a248e569 -server/routes/auth.py,"Defines FastAPI authentication routes for checking auth status, logging out, initiating GitHub Device Flow login, polling login progress, and testing cloud connectivity.","[""router"", ""AuthStatus"", ""LogoutResult"", ""LoginRequest"", ""LoginResponse"", ""LoginPollResponse"", ""JWTTokenResponse"", ""CloudConnectionTestResponse"", ""get_auth_status"", ""get_jwt_token"", ""logout"", ""start_login"", ""poll_login_status"", ""test_cloud_connection""]","[""os"", ""time"", ""uuid"", ""webbrowser"", ""typing"", ""fastapi"", ""pydantic"", ""pdd""]",629e936f6fd2d96b55669cdb31bfcb9cbf93dbf08c859477007c46ce56a5ffc7 -server/routes/commands.py,"Defines FastAPI REST endpoints for submitting, monitoring, canceling, and spawning PDD CLI commands via async job management and terminal subprocesses.","[""router"", ""RunResult"", ""CancelResult"", ""ProcessTracker"", ""SpawnTerminalResponse"", ""SpawnedJobCompleteRequest"", ""SpawnedJobCompleteResponse"", ""SpawnedJobStatus"", ""ALLOWED_COMMANDS"", ""POSITIONAL_ARGS"", ""GLOBAL_OPTIONS"", ""set_job_manager"", ""get_job_manager"", ""set_project_root"", ""get_project_root"", ""set_server_port""]","[""asyncio"", ""os"", ""signal"", ""subprocess"", ""sys"", ""threading"", ""time"", ""uuid"", ""datetime"", ""pathlib"", ""typing"", ""fastapi"", ""rich"", ""pydantic""]",c70fd869490688b3a9b0ea39cdddf301f93abdcfd364c4d722232a6dc820c0f2 -server/routes/config.py,Defines a FastAPI route that exposes the server's cloud URL and environment configuration to frontend clients.,"[""router"", ""CloudUrlResponse"", ""get_cloud_url""]","[""fastapi"", ""pydantic"", ""pdd""]",efe80f308a6a2acdb6a207288f746d39f0947da597c44c358595ff7b70e587d9 -server/routes/extracts.py,"Defines FastAPI REST endpoints for listing, inspecting, freshness-checking, and pruning cached LLM extraction results stored in .pdd/extracts/.","[""router"", ""ExtractMetadata"", ""ExtractContent"", ""ExtractListResponse"", ""PromptExtractInfo"", ""PruneResponse"", ""get_project_root"", ""set_project_root"", ""list_extracts"", ""extracts_for_prompt"", ""prune_extracts"", ""get_extract""]","[""hashlib"", ""json"", ""os"", ""re"", ""datetime"", ""pathlib"", ""typing"", ""fastapi"", ""pydantic"", ""rich""]",1308d4b7c79d5560e31d25c60820405c1e5f3ef2560eb03674f0fb4e9cf03794 -server/routes/files.py,"Defines FastAPI REST API endpoints for browsing file trees, reading/writing file content, listing and detecting changed prompt files, and retrieving file metadata with security validation.","[""router"", ""get_path_validator"", ""set_path_validator"", ""BINARY_EXTENSIONS"", ""DEFAULT_CHUNK_SIZE"", ""KNOWN_LANGUAGES"", ""LANGUAGE_EXTENSIONS"", ""load_pddrc"", ""match_context"", ""parse_prompt_stem"", ""get_file_tree"", ""get_file_content"", ""write_file"", ""list_prompt_files"", ""list_changed_prompt_files""]","[""base64"", ""hashlib"", ""datetime"", ""pathlib"", ""typing"", ""fastapi"", ""rich""]",ae18eb66ee106fa28668d392fc4b599fd00b28c1ae46f2c5c54deb9b88ae97c0 -server/routes/prompts.py,"Defines FastAPI REST endpoints and Pydantic models for prompt analysis, token metrics, sync status, model listing, match checking, diff analysis, and git-based prompt version history.","[""router"", ""CostEstimateResponse"", ""TokenMetricsResponse"", ""PromptAnalyzeRequest"", ""PromptAnalyzeResponse"", ""SyncStatusResponse"", ""ModelInfo"", ""ModelsResponse"", ""MatchCheckRequest"", ""MatchCheckResponse"", ""DiffAnalysisRequest"", ""DiffAnalysisResponse"", ""PromptDiffRequest"", ""PromptDiffResponse"", ""set_path_validator""]","[""os"", ""pathlib"", ""typing"", ""fastapi"", ""pydantic"", ""rich"", ""pdd""]",7636536d90533e1ccac0703c9283304702b4167b6eedbc23d7656e9105f886ad -server/routes/websocket.py,"Provides WebSocket endpoints and a connection manager for streaming job output, handling user input/cancellation, and broadcasting file system change events via FastAPI.","[""router"", ""ConnectionManager"", ""manager"", ""AsyncFileEventHandler"", ""clean_ansi"", ""emit_job_output"", ""emit_job_progress"", ""emit_job_complete"", ""emit_spawned_job_complete"", ""create_websocket_routes"", ""ANSI_ESCAPE"", ""DEBOUNCE_SECONDS""]","[""asyncio"", ""json"", ""re"", ""time"", ""datetime"", ""pathlib"", ""typing"", ""fastapi"", ""rich"", ""watchdog""]",961fbae5ff5944b4be4104be65320c8bfb3bda62bff4db6eeaf4df8b1da1039a -server/security.py,"Provides security utilities for a FastAPI application, including path validation with blacklisting, CORS configuration, Bearer token authentication, and request logging middleware.","[""DEFAULT_BLACKLIST"", ""SecurityError"", ""PathValidator"", ""configure_cors"", ""create_token_dependency"", ""SecurityLoggingMiddleware""]","[""fnmatch"", ""time"", ""pathlib"", ""typing"", ""fastapi"", ""starlette"", ""rich""]",5e64f8aa746d9642f428c59366ecf1c743d0fd77606ac60b5d8187da8737358e -server/terminal_spawner.py,"Provides cross-platform terminal window spawning with command execution and optional server callback for job completion tracking on macOS, Linux, and Windows.","[""TerminalSpawner"", ""DEFAULT_SERVER_PORT""]","[""os"", ""shutil"", ""subprocess"", ""sys"", ""pathlib"", ""typing""]",efffd485569bb2f2e673b408c607a02a9b3325f46d2db8af2f4f8f8439f49fb5 -server/token_counter.py,"Provides token counting, context window lookup, and cost estimation utilities using litellm with tiktoken as a fallback.","[""CostEstimate"", ""TokenMetrics"", ""count_tokens"", ""get_context_limit"", ""estimate_cost"", ""get_token_metrics""]","[""csv"", ""dataclasses"", ""functools"", ""pathlib"", ""typing"", ""litellm"", ""tiktoken""]",c94fce0132ef534f63d08f4763e801825ba0b15d595987e8535f1a9bb5d75f6b -setup_tool.py,"Orchestrates the interactive `pdd setup` command through a two-phase flow: CLI bootstrap with user prompts, then deterministic auto-configuration of API keys, models, and project files.","[""run_setup""]","[""getpass"", ""os"", ""sys"", ""pathlib"", ""typing"", ""rich"", ""pdd""]",d00d304fd60bdab90275b6699040a718d94d820808d1b4b21047537f1d454a2a -split.py,Splits a prompt into extracted sub-module functionality and a remaining prompt using two sequential LLM invocations with structured Pydantic output.,"[""PromptSplit"", ""split""]","[""typing"", ""rich"", ""pydantic""]",f655ab87e24e8cea71469e3eb3554bedba893155a5b266392f138d4fbb1833c0 -split_main.py,"CLI wrapper that orchestrates splitting a prompt into extracted functionality and remaining prompt, handling file I/O, error reporting, and user feedback.","[""split_main""]","[""sys"", ""typing"", ""click"", ""rich""]",e76adc668792ff0a58462a9ba8c520aff85463b192eb6a2dc330dfb801a2e980 -summarize_directory.py,"Summarizes all files in a directory using an LLM, with content-hash-based caching and CSV output for incremental progress.","[""summarize_directory"", ""FileSummary"", ""BINARY_EXTENSIONS""]","[""glob"", ""hashlib"", ""io"", ""csv"", ""os"", ""subprocess"", ""json"", ""typing"", ""pydantic"", ""rich""]",3fd0f28a8f42eaa60973698b737fba46774a6105b6b8b7afe5c8e4224a0696bb -sync_animation.py,"Renders an animated terminal UI showing an org-chart-style diagram of PDD workflow boxes (Prompt, Code, Example, Tests) with connecting arrows, scrolling paths, cost tracking, and blinking emojis using Rich.","[""AnimationState"", ""sync_animation"", ""EMOJIS"", ""PDD_LOGO_ASCII"", ""CONSOLE_WIDTH"", ""ANIMATION_BOX_HEIGHT"", ""DEEP_NAVY"", ""ELECTRIC_CYAN"", ""LUMEN_PURPLE"", ""PROMPT_MAGENTA"", ""BUILD_GREEN""]","[""time"", ""os"", ""datetime"", ""threading"", ""typing"", ""rich""]",e60160f06f0f28cb492e449c395f7c3800f24212cc9f0eab25fd29a99c408906 -sync_determine_operation.py,"Implements fingerprint-based state analysis and deterministic operation selection for the PDD sync command, deciding which workflow step (generate, test, fix, etc.) to run next.","[""sync_determine_operation"", ""analyze_conflict_with_llm"", ""read_run_report"", ""get_pdd_file_paths"", ""Fingerprint"", ""RunReport"", ""SyncDecision"", ""SyncLock"", ""PDD_DIR"", ""META_DIR"", ""LOCKS_DIR"", ""get_pdd_dir"", ""get_meta_dir"", ""get_locks_dir"", ""calculate_sha256""]","[""os"", ""re"", ""sys"", ""json"", ""hashlib"", ""subprocess"", ""fnmatch"", ""pathlib"", ""dataclasses"", ""typing"", ""datetime"", ""psutil"", ""pdd"", ""logging""]",da10acc6a41c72f0afcb2d6137d78bd912c5feb18fe80e41895559de06139f55 -sync_main.py,"Implements the main CLI sync workflow for PDD, handling basename validation, multi-language prompt discovery, path resolution, budget tracking, and orchestration of code synchronization across detected languages.","[""sync_main"", ""SUPPORTED_SYNC_LANGUAGES"", ""VALID_BASENAME_CHARS"", ""_validate_basename"", ""_detect_languages"", ""_detect_languages_with_context"", ""_find_prompt_in_contexts"", ""_normalize_prompts_root"", ""_python_first_sorted"", ""_extract_prompts_base_dir"", ""_relative_basename_for_context""]","[""fnmatch"", ""re"", ""time"", ""pathlib"", ""typing"", ""click"", ""rich""]",4beb957bbebb1f3256988c51e3df723d05d74bb6e0cff80b906fe6ed25d69aef -sync_orchestration.py,"Orchestrates the PDD sync workflow by coordinating code generation, testing, crash detection, fixing, and verification operations with parallel TUI animation and atomic state management.","[""sync_orchestration"", ""AtomicStateUpdate"", ""PendingStateUpdate"", ""MAX_CONSECUTIVE_TESTS"", ""MAX_TEST_EXTEND_ATTEMPTS"", ""MAX_CONSECUTIVE_CRASHES""]","[""threading"", ""time"", ""json"", ""datetime"", ""subprocess"", ""re"", ""os"", ""pathlib"", ""typing"", ""dataclasses"", ""tempfile"", ""sys"", ""click"", ""logging""]",6a24a89af727bcd12d94896d38a05baed7fbe155d0cd7c2d266dc87decc6d22a -sync_order.py,"Builds a dependency graph from prompt file includes, performs topological sorting, identifies affected modules, and generates ordered sync shell scripts.","[""extract_includes_from_file"", ""extract_module_from_include"", ""build_dependency_graph"", ""topological_sort"", ""get_affected_modules"", ""generate_sync_order_script""]","[""os"", ""re"", ""stat"", ""logging"", ""datetime"", ""pathlib"", ""typing"", ""collections"", ""rich"", ""pdd""]",97c131ddb0d085eaeb68b8ccfc5e674c1027f40d71639be03417a92d2508bc70 -sync_tui.py,"Provides a Textual-based terminal UI for PDD sync operations, including animated displays, modal dialogs for confirmation/input/choice, stdout/stdin redirection, and user steering of sync workflows.","[""SyncApp"", ""ChoiceScreen"", ""ConfirmScreen"", ""InputScreen"", ""ThreadSafeRedirector"", ""TUIStdoutWrapper"", ""TUIStdinRedirector"", ""show_exit_animation"", ""maybe_steer_operation"", ""DEFAULT_STEER_TIMEOUT_S""]","[""threading"", ""sys"", ""os"", ""typing"", ""io"", ""asyncio"", ""textual"", ""rich"", ""time"", ""re""]",ee30cc819ab17b744224126564010e22e4d179f3e5dcedf056f591822cd03ca9 -template_expander.py,"Expands path templates with placeholders like {name}, {category}, and {ext}, supporting case conversions (snake, pascal, kebab) and path normalization.","[""expand_template""]","[""re"", ""os"", ""typing""]",bcaa670f334b9fa7726aa85906e70125ad2cd4b7e7b62a8e39f8bdd06c975edb -template_registry.py,"Discovers, indexes, and manages prompt templates from packaged and project sources, supporting listing, loading, inspecting, and copying operations with YAML front-matter metadata parsing.","[""TemplateMeta"", ""list_templates"", ""load_template"", ""show_template"", ""copy_template""]","[""re"", ""shutil"", ""collections"", ""dataclasses"", ""pathlib"", ""typing"", ""importlib""]",2fc650f9aa667437bb263fc99601efedf528222267183b84d15eb863e30ef6f0 -trace.py,Traces a line of generated code back to its originating line in a prompt file using LLM analysis and fuzzy string matching.,"[""trace"", ""PromptLineOutput""]","[""typing"", ""rich"", ""pydantic"", ""difflib"", ""re""]",9ff879812f1def2c14995b939a3b6541590ee68a7f62b25e57064ac7d642fbcb -trace_main.py,"Implements the core logic for the CLI 'trace' command, which traces a code line back to its originating prompt line and optionally saves results.","[""trace_main""]","[""click"", ""rich"", ""typing"", ""os"", ""logging""]",48e9cc107b5af5668c031ba532b93a685a2399ae2d8bad482ef269dcd561a5b8 -track_cost.py,"Provides a decorator that tracks CLI command costs, collects input/output file paths, and logs execution metadata to a CSV file.","[""track_cost"", ""extract_cost_and_model"", ""collect_files""]","[""functools"", ""datetime"", ""csv"", ""os"", ""click"", ""rich"", ""typing""]",63ad7826cd7b69f85d717c38ea1f019714625e63c3725ddf3a63f99ba588d4c6 -unfinished_prompt.py,"Analyzes whether a given prompt text is complete or truncated/unfinished, using both fast Python syntax checks and LLM-based assessment.","[""PromptAnalysis"", ""unfinished_prompt""]","[""typing"", ""ast"", ""textwrap"", ""warnings"", ""pydantic"", ""rich""]",b012d1c6fab0579ae674d2d6e26be119dd9288c7ba2741b75af516be7a4564bf -update_main.py,"Provides CLI logic for updating prompt files from code changes, supporting single-file updates, repository-wide scans with drift detection, dependency ordering, architecture/PRD sync, and agentic or legacy LLM pipelines.","[""resolve_prompt_code_pair"", ""find_and_resolve_all_pairs"", ""get_git_changed_files"", ""derive_basename_and_language"", ""is_code_changed"", ""update_file_pair"", ""update_main""]","[""fnmatch"", ""re"", ""subprocess"", ""sys"", ""heapq"", ""collections"", ""typing"", ""click"", ""rich"", ""os"", ""pathlib"", ""git""]",d82676d754e4fe5c0b1282c9482779ee84a11016ca14ffb4d211bc83533c9305 -update_model_costs.py,"CLI script that reads an LLM model CSV file and updates missing cost and structured output data using LiteLLM, with rich console reporting.","[""update_model_data"", ""main"", ""EXPECTED_COLUMNS"", ""INT_COLUMNS""]","[""argparse"", ""os"", ""pandas"", ""litellm"", ""rich"", ""math"", ""pathlib""]",45f78e9401ed735142c78ec0ec28eb1f6b7959741c950ffdb98b4d8ed421ef92 -update_prompt.py,Updates an LLM prompt based on differences between original and modified code using a two-step LLM invocation pipeline.,"[""PromptUpdate"", ""update_prompt""]","[""typing"", ""rich"", ""pydantic""]",2b58bd048a5bde2a0518cdf659caae780b8aab32e57b3b0fb2f728bce6a03a12 -user_story_tests.py,"Manages user story lifecycle including discovery, prompt linking via metadata, story generation from prompt files, story-based test validation, and automated prompt fixing.","[""DEFAULT_STORIES_DIR"", ""DEFAULT_PROMPTS_DIR"", ""DEFAULT_SRC_DIR"", ""STORY_PREFIX"", ""STORY_SUFFIX"", ""STORY_PROMPTS_METADATA_KEY"", ""discover_story_files"", ""discover_prompt_files"", ""cache_story_prompt_links"", ""generate_user_story"", ""run_user_story_tests"", ""run_user_story_fix""]","[""logging"", ""os"", ""re"", ""pathlib"", ""typing"", ""rich""]",04f84ddc72d9ce597c9c8f87cfcbaf9496c5cb03f72d6042a8fd334972ce20c5 -xml_tagger.py,Enhances LLM prompts by adding XML tags for improved structure and readability using a two-step LLM invocation pipeline.,"[""XMLOutput"", ""xml_tagger"", ""main""]","[""typing"", ""rich"", ""pydantic""]",e417371d19bb8b3fd78c177340870c6fc2a871443c3c42c8d8d3791b0e1a99de +.env.example,Configuration file for API keys of various LLM providers and project paths.,[],[],737cd7ac80451973147fdcbd24a8d3e150f0b493ba55ff22a1d519b098dd0d55 +.github/workflows/notify-private.yml,Defines a GitHub Actions workflow that triggers a sync event to a private repository on pushes to the main branch.,[],[],bd3b201e292cd57e9adc978415888226187a59a37a70851041ecb91a87be262e +.github/workflows/pdd-secrets-dispatch.yml,GitHub Actions workflow to encrypt and send repository secrets to a callback URL upon dispatch.,[],"[""os"", ""json"", ""base64"", ""secrets"", ""requests"", ""cryptography""]",426daed826ee00bb364fa9dc04fc59aca2dcc169b9a02558071651cb583065b5 +.github/workflows/unit-tests.yml,Defines a GitHub Actions workflow to run unit tests on pushes and pull requests to the main branch.,[],[],452a99d69da271bbd7a7950af6011fe68bb3751001481726d6c315e5da7f67c6 +.gitignore,"This file specifies patterns for Git to ignore various temporary files, caches, builds, and directories in a development project.",[],[],4fe6c5371c8411b0d76439cded6e7c0621c581154520a5d84c7c9d68e5c8a306 +.pddrc,Configures contexts and default settings for various components of the PDD CLI codebase.,[],[],3d43d7f7bf7666810230eefe09b53fc0bbb236882dddc56a8ae48985ce93aa77 +CHANGELOG.md,"Changelog documenting updates, features, fixes, and refactors for the Prompt-Driven Development (PDD) project across versions v0.0.179 to v0.0.23.",[],[],95de96a1931dfc0b880333b55e0f1efe5a5bbdb9ece05edf2499f96f51f235db +CONTRIBUTING.md,"This file outlines guidelines for contributing to the PDD CLI project, covering setup, workflow, philosophy, and best practices.",[],[],1cf52ca8e1809a744674b676eeef94eebfc7bd9d940b1ddeb861943eb7bf9566 +LICENSE,"Grants permission to use, modify, and distribute the software under the MIT license with copyright notice.",[],[],92f4c99e7c4f55362518a498e1937593376627497145d356c60ba9681dbbcec5 +Makefile,"Makefile for the PDD project, providing targets for code generation, testing, building, publishing, and maintenance tasks.","[""help"", ""generate"", ""example"", ""run-examples"", ""test"", ""coverage"", ""regression"", ""sync-regression"", ""all-regression"", ""cloud-regression"", ""verify"", ""lint"", ""update"", ""fix"", ""crash""]",[],215a92c25a03ba5a6f60fb447fd14d7251e03aeb0d590e57552a472502f86cc1 diff --git a/tests/core/test_cli.py b/tests/core/test_cli.py index ef8260784..3f066d750 100644 --- a/tests/core/test_cli.py +++ b/tests/core/test_cli.py @@ -499,6 +499,16 @@ def test_strip_ansi_codes_removes_complex_sequences(): text = "\x1b[1;31;42mComplex\x1b[0m" assert _strip_ansi_codes(text) == "Complex" +def test_strip_ansi_codes_removes_osc_sequences(): + # OSC title set + BEL terminator + text = "pre\x1b]0;mytitle\x07post" + assert _strip_ansi_codes(text) == "prepost" + +def test_strip_ansi_codes_removes_cursor_sequences(): + # CSI cursor movement and erase-in-line + text = "a\x1b[2Kb\x1b[1Dc" + assert _strip_ansi_codes(text) == "abc" + def test_strip_ansi_codes_leaves_plain_text(): text = "Just plain text" assert _strip_ansi_codes(text) == text diff --git a/tests/test_core_dump.py b/tests/test_core_dump.py index e196f877e..52158c25f 100644 --- a/tests/test_core_dump.py +++ b/tests/test_core_dump.py @@ -111,6 +111,7 @@ def test_core_dump_includes_file_contents(tmp_path, monkeypatch): # Read and verify content core_dump_data = json.loads(core_dumps[0].read_text()) + assert core_dump_data.get("schema_version") == 2 # Check that file contents are included assert 'file_contents' in core_dump_data @@ -181,6 +182,7 @@ def test_core_dump_auto_includes_meta_files(tmp_path, monkeypatch): # Read and verify content core_dump_data = json.loads(core_dumps[0].read_text()) + assert core_dump_data.get("schema_version") == 2 # Meta file should be auto-included file_contents = core_dump_data.get('file_contents', {}) @@ -188,6 +190,90 @@ def test_core_dump_auto_includes_meta_files(tmp_path, monkeypatch): f"Meta file not auto-included: {list(file_contents.keys())}" +def test_core_dump_auto_includes_operation_log_and_run_report(tmp_path, monkeypatch): + """Core dump should include .pdd/meta/*_sync.log and *_run.json when present.""" + import json + from pdd.core.dump import _write_core_dump + + meta_dir = tmp_path / ".pdd" / "meta" + meta_dir.mkdir(parents=True, exist_ok=True) + (meta_dir / "demo_python_sync.log").write_text( + '{"operation":"fix","reason":"x","success":false,"duration":1.23,"actual_cost":0.02,"model":"m","error":"boom","details":{"test_output_excerpt":"out"}}\n' + ) + (meta_dir / "demo_python_run.json").write_text('{"exit_code": 1, "tests_failed": 1}\n') + + mock_ctx = MagicMock() + mock_ctx.obj = { + "core_dump": True, + "core_dump_files": set(), + "force": False, + "strength": 0.75, + "temperature": 0.0, + "time": 0.25, + "verbose": False, + "quiet": True, + "local": False, + "context": None, + "output_cost": None, + "review_examples": False, + } + + monkeypatch.chdir(tmp_path) + _write_core_dump(mock_ctx, [("ok", 0.0, "")], ["sync"], 0.0) + + core_dumps = list((tmp_path / ".pdd" / "core_dumps").glob("pdd-core-*.json")) + core_dump_data = json.loads(core_dumps[0].read_text()) + file_contents = core_dump_data.get("file_contents", {}) + + assert any(k.endswith("_sync.log") for k in file_contents.keys()), f"Missing *_sync.log: {list(file_contents.keys())}" + assert any(k.endswith("_run.json") for k in file_contents.keys()), f"Missing *_run.json: {list(file_contents.keys())}" + + # Expanded per-operation steps should be present. + sync_steps = core_dump_data.get("sync_steps") or [] + assert len(sync_steps) >= 1 + assert sync_steps[0]["operation"] == "fix" + assert sync_steps[0]["success"] is False + assert sync_steps[0]["model"] == "m" + assert "boom" in str(sync_steps[0].get("failure_summary")) + assert sync_steps[0].get("test_output_excerpt") == "out" + # LLM trace should also be carried through when present. + assert sync_steps[0].get("source_log") + + +def test_core_dump_steps_model_default_unknown(tmp_path, monkeypatch): + """If results omit model or results are missing, core dump should store model='unknown'.""" + import json + from pdd.core.dump import _write_core_dump + + mock_ctx = MagicMock() + mock_ctx.obj = { + "core_dump": True, + "core_dump_files": set(), + "force": False, + "strength": 0.75, + "temperature": 0.0, + "time": 0.25, + "verbose": False, + "quiet": True, + "local": False, + "context": None, + "output_cost": None, + "review_examples": False, + } + + monkeypatch.chdir(tmp_path) + # Two invoked subcommands but only one result, and it has an empty model. + _write_core_dump(mock_ctx, [("r1", 0.1, "")], ["sync", "generate"], 0.1) + + core_dumps = list((tmp_path / ".pdd" / "core_dumps").glob("pdd-core-*.json")) + core_dump_data = json.loads(core_dumps[0].read_text()) + steps = core_dump_data.get("steps", []) + + assert len(steps) == 2 + assert steps[0]["model"] == "unknown" + assert steps[1]["model"] == "unknown" + + def test_core_dump_handles_large_files(tmp_path, monkeypatch): """Test that core dump marks large files as 'too large'.""" import json @@ -342,7 +428,7 @@ def test_terminal_output_included_in_gist(tmp_path): # Create a payload with terminal output payload = { - "schema_version": 1, + "schema_version": 2, "pdd_version": "1.0.0", "timestamp_utc": "20231201T120000Z", "terminal_output": "Test terminal output\nLine 2\nLine 3", @@ -385,7 +471,7 @@ def test_terminal_output_in_issue_markdown(tmp_path): from pdd.core.dump import _build_issue_markdown payload = { - "schema_version": 1, + "schema_version": 2, "pdd_version": "1.0.0", "timestamp_utc": "20231201T120000Z", "argv": ["generate", "test.prompt"], diff --git a/tests/test_core_errors.py b/tests/test_core_errors.py index 74af5ac84..8862dc4ed 100644 --- a/tests/test_core_errors.py +++ b/tests/test_core_errors.py @@ -158,3 +158,22 @@ def test_keyboard_interrupt_reports_correct_command_name( assert "'unknown'" not in output, ( f"Command name should not be 'unknown': {output}" ) + + +def test_record_core_dump_error_adds_structured_entry(): + from pdd.core.errors import clear_core_dump_errors, get_core_dump_errors, record_core_dump_error + + clear_core_dump_errors() + record_core_dump_error( + command="sync", + type="LogicalFailure", + message="Budget exhausted", + details={"remaining_budget": 0.0}, + ) + + errs = get_core_dump_errors() + assert len(errs) == 1 + assert errs[0]["command"] == "sync" + assert errs[0]["type"] == "LogicalFailure" + assert errs[0]["message"] == "Budget exhausted" + assert errs[0]["details"]["remaining_budget"] == 0.0 diff --git a/tests/test_issue_633_reproduction.py b/tests/test_issue_633_reproduction.py index b3598232f..cc766c5df 100644 --- a/tests/test_issue_633_reproduction.py +++ b/tests/test_issue_633_reproduction.py @@ -358,9 +358,9 @@ def test_http_method_guidance_exists( self, step11_prompt_content: str ) -> None: """ - Confirm the prompt contains guidance about HTTP methods in mocks. + Confirm the prompt contains explicit HTTP method guidance in mocks. - Updated after fix: the API Mocking Best Practices section now provides + After the issue #633 fix, the API Mocking Best Practices section includes explicit HTTP method guidance (do NOT assume REST conventions). """ content_lower = step11_prompt_content.lower() @@ -370,5 +370,5 @@ def test_http_method_guidance_exists( "don't assume rest" in content_lower, ]) assert has_http_method_guidance, ( - "Prompt should contain HTTP method guidance after the fix." + "Prompt should contain HTTP method guidance after the issue #633 fix." ) diff --git a/tests/test_sync_orchestration.py b/tests/test_sync_orchestration.py index add496004..8091d82ea 100644 --- a/tests/test_sync_orchestration.py +++ b/tests/test_sync_orchestration.py @@ -11,6 +11,7 @@ from pdd.sync_orchestration import sync_orchestration, _execute_tests_and_create_run_report, _try_auto_fix_env_var_error from pdd.sync_determine_operation import SyncDecision, get_pdd_file_paths +from pdd.get_test_command import TestCommand # Test Plan: # The sync_orchestration module is the central coordinator for the `pdd sync` command. @@ -1652,6 +1653,211 @@ def test_passes(): ) +def test_sync_orchestration_records_logical_failure_in_core_dump_errors(tmp_path, monkeypatch): + """If sync breaks due to consecutive fix loop protection, it should record structured errors.""" + import json + from unittest.mock import patch, MagicMock + + from pdd.core.errors import clear_core_dump_errors, get_core_dump_errors + from pdd.sync_determine_operation import SyncDecision + from pdd.sync_orchestration import sync_orchestration + + clear_core_dump_errors() + monkeypatch.chdir(tmp_path) + + # Minimal project structure/files so orchestration doesn't crash on missing paths + (tmp_path / "prompts").mkdir() + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + (tmp_path / "context").mkdir() + (tmp_path / ".pdd" / "meta").mkdir(parents=True) + + prompt = tmp_path / "prompts" / "demo_python.prompt" + code = tmp_path / "src" / "demo.py" + example = tmp_path / "context" / "demo_example.py" + test_file = tmp_path / "tests" / "test_demo.py" + prompt.write_text("Create demo") + code.write_text("print('x')\n") + example.write_text("print('example')\n") + test_file.write_text("def test_ok(): assert True\n") + + fake_paths = { + "prompt": prompt, + "code": code, + "example": example, + "test": test_file, + "test_files": [test_file], + } + + # Return 'fix' repeatedly so we eventually trigger the consecutive-fix breaker. + decisions = [SyncDecision(operation="fix", reason="tests failing")] * 10 + + with patch("pdd.sync_orchestration.get_pdd_file_paths", return_value=fake_paths), \ + patch("pdd.sync_orchestration.SyncLock") as mock_lock, \ + patch("pdd.sync_orchestration.sync_determine_operation", side_effect=decisions), \ + patch("pdd.sync_orchestration.extract_failing_files_from_output", return_value=[]), \ + patch("pdd.get_test_command.get_test_command_for_file", return_value=None), \ + patch("pdd.sync_orchestration.fix_main", return_value=(True, None, None, 1, 0.01, "mock-model")), \ + patch("pdd.sync_orchestration._save_fingerprint_atomic"), \ + patch("pdd.sync_orchestration.append_log_entry"), \ + patch("pdd.sync_orchestration.log_event"): + + mock_lock.return_value.__enter__.return_value = mock_lock + mock_lock.return_value.__exit__.return_value = None + + # Force headless mode so we don't invoke the TUI. + result = sync_orchestration(basename="demo", language="python", quiet=True, prompts_dir="prompts") + + assert result["success"] is False + + errs = get_core_dump_errors() + # Should include at least one LogicalFailure for the consecutive fix breaker. + assert any(e.get("type") == "LogicalFailure" and "consecutive fix" in (e.get("message") or "") for e in errs), errs + # And a final SyncFailed entry with details. + assert any(e.get("type") == "SyncFailed" and e.get("details", {}).get("basename") == "demo" for e in errs), errs + + +def test_sync_orchestration_fix_captures_truncated_test_output_excerpt(tmp_path, monkeypatch): + """Failing test run during fix should be captured (truncated ~5KB) into sync log entry details.""" + from unittest.mock import patch + import subprocess + + from pdd.sync_determine_operation import SyncDecision + from pdd.sync_orchestration import sync_orchestration + + monkeypatch.chdir(tmp_path) + + # Minimal project structure/files + (tmp_path / "prompts").mkdir() + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + (tmp_path / "context").mkdir() + (tmp_path / ".pdd" / "meta").mkdir(parents=True) + + prompt = tmp_path / "prompts" / "demo_ts.prompt" + code = tmp_path / "src" / "demo.ts" + example = tmp_path / "context" / "demo_example.ts" + test_file = tmp_path / "tests" / "test_demo.ts" + prompt.write_text("Create demo", encoding="utf-8") + code.write_text("export const x = 1;\n", encoding="utf-8") + example.write_text("console.log('example')\n", encoding="utf-8") + test_file.write_text("test('ok', () => expect(true).toBe(true));\n", encoding="utf-8") + + fake_paths = { + "prompt": prompt, + "code": code, + "example": example, + "test": test_file, + "test_files": [test_file], + } + + # Force: fix, then terminate workflow + decisions = [ + SyncDecision(operation="fix", reason="tests failing"), + SyncDecision(operation="all_synced", reason="done"), + ] + + # Very long output to force truncation + long_out = "X" * (7 * 1024) + long_err = "Y" * (7 * 1024) + failing_cp = subprocess.CompletedProcess(args=["cmd"], returncode=1, stdout=long_out, stderr=long_err) + + seen_log_entries = [] + + def capture_append(_basename, _language, entry): + seen_log_entries.append(entry) + + with patch("pdd.sync_orchestration.get_pdd_file_paths", return_value=fake_paths), \ + patch("pdd.sync_orchestration.SyncLock") as mock_lock, \ + patch("pdd.sync_orchestration.sync_determine_operation", side_effect=decisions), \ + patch("pdd.sync_orchestration.extract_failing_files_from_output", return_value=[]), \ + patch("pdd.get_test_command.get_test_command_for_file", return_value=TestCommand(command="echo run-tests")), \ + patch("pdd.sync_orchestration._run_fix_operation_test_subprocess", return_value=failing_cp), \ + patch("pdd.sync_orchestration.fix_main", return_value=(False, None, None, 1, 0.01, "mock-model")), \ + patch("pdd.sync_orchestration._save_fingerprint_atomic"), \ + patch("pdd.sync_orchestration.append_log_entry", side_effect=capture_append), \ + patch("pdd.sync_orchestration.log_event"): + + mock_lock.return_value.__enter__.return_value = mock_lock + mock_lock.return_value.__exit__.return_value = None + + # Quiet => headless path (no TUI) + result = sync_orchestration(basename="demo", language="typescript", quiet=True, prompts_dir="prompts") + + assert result["success"] is False + + # Find the fix operation entry and validate excerpt exists + is truncated + fix_entries = [e for e in seen_log_entries if e.get("operation") == "fix"] + assert fix_entries, f"No fix log entry captured. Entries: {seen_log_entries}" + entry = fix_entries[0] + excerpt = (entry.get("details") or {}).get("test_output_excerpt") + assert isinstance(excerpt, str) and excerpt, "Expected non-empty test_output_excerpt" + assert len(excerpt) <= 5 * 1024 + 200 # allow small truncation suffix + assert "truncated" in excerpt + + +def test_sync_orchestration_attaches_llm_trace_on_failed_operation(tmp_path, monkeypatch): + """When an operation fails and llm_invoke recorded a pair, it should be attached to sync log entry details.""" + from unittest.mock import patch + + from pdd.sync_determine_operation import SyncDecision + from pdd.sync_orchestration import sync_orchestration + + monkeypatch.chdir(tmp_path) + (tmp_path / "prompts").mkdir() + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + (tmp_path / "context").mkdir() + (tmp_path / ".pdd" / "meta").mkdir(parents=True) + + prompt = tmp_path / "prompts" / "demo_python.prompt" + code = tmp_path / "src" / "demo.py" + example = tmp_path / "context" / "demo_example.py" + test_file = tmp_path / "tests" / "test_demo.py" + prompt.write_text("Create demo", encoding="utf-8") + code.write_text("print('x')\n", encoding="utf-8") + example.write_text("print('example')\n", encoding="utf-8") + test_file.write_text("def test_ok(): assert True\n", encoding="utf-8") + + fake_paths = { + "prompt": prompt, + "code": code, + "example": example, + "test": test_file, + "test_files": [test_file], + } + + decisions = [ + SyncDecision(operation="generate", reason="force"), + SyncDecision(operation="error", reason="stop"), + ] + + seen_entries = [] + + def capture_append(_b, _l, entry): + seen_entries.append(entry) + + # Pretend llm trace was recorded for this op (without calling real llm). + fake_trace = {"prompt": "P", "response": "R", "model": "m"} + + with patch("pdd.sync_orchestration.get_pdd_file_paths", return_value=fake_paths), \ + patch("pdd.sync_orchestration.SyncLock") as mock_lock, \ + patch("pdd.sync_orchestration.sync_determine_operation", side_effect=decisions), \ + patch("pdd.sync_orchestration.code_generator_main", return_value=(None, False, 0.01, "m")), \ + patch("pdd.sync_orchestration.pop_last_pair", return_value=fake_trace), \ + patch("pdd.sync_orchestration.append_log_entry", side_effect=capture_append), \ + patch("pdd.sync_orchestration.log_event"): + + mock_lock.return_value.__enter__.return_value = mock_lock + mock_lock.return_value.__exit__.return_value = None + res = sync_orchestration(basename="demo", language="python", quiet=True, prompts_dir="prompts") + + assert res["success"] is False + gen_entries = [e for e in seen_entries if e.get("operation") == "generate"] + assert gen_entries, f"No generate entry: {seen_entries}" + details = gen_entries[0].get("details") or {} + assert details.get("llm_trace") == fake_trace + # --- Coverage Target Selection Regression Tests --- class TestCoverageTargetSelection: @@ -2812,9 +3018,9 @@ def test_fix_operation_identifies_actual_failing_test_file(orchestration_fixture mock_result.stderr = "" mock_result.returncode = 1 - with patch('pdd.sync_orchestration.subprocess.run', return_value=mock_result): + with patch('pdd.sync_orchestration._run_fix_operation_test_subprocess', return_value=mock_result): with patch('pdd.sync_orchestration.detect_host_python_executable', return_value='python'): - with patch('pdd.get_test_command.get_test_command_for_file', return_value='pytest'): + with patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command='pytest')): result = sync_orchestration(basename="calculator", language="python") # ASSERTION: fix_main should receive the ACTUAL failing file @@ -2885,9 +3091,9 @@ def capture_subprocess(args, **kwargs): SyncDecision(operation='all_synced', reason='Done'), ] - with patch('pdd.sync_orchestration.subprocess.run', side_effect=capture_subprocess): + with patch('pdd.sync_orchestration._run_fix_operation_test_subprocess', side_effect=capture_subprocess): with patch('pdd.sync_orchestration.detect_host_python_executable', return_value='python'): - with patch('pdd.get_test_command.get_test_command_for_file', return_value='pytest'): + with patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command='pytest')): sync_orchestration(basename="calculator", language="python") # Verify pytest was called @@ -3604,8 +3810,8 @@ def test_python_fix_uses_default_max_attempts(self, non_python_fixture): patch('pdd.sync_orchestration.save_run_report'), \ patch('pdd.sync_orchestration._display_sync_log'), \ patch('pdd.sync_orchestration._save_fingerprint_atomic'), \ - patch('pdd.get_test_command.get_test_command_for_file', return_value="pytest"), \ - patch('pdd.sync_orchestration.subprocess.run') as mock_subprocess, \ + patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command="pytest")), \ + patch('pdd.sync_orchestration._run_fix_operation_test_subprocess') as mock_subprocess, \ patch.object(sys.stdout, 'isatty', return_value=True): self._setup_sync_app_mock(mock_sync_app_class) @@ -3718,8 +3924,8 @@ def test_non_python_fix_uses_max_attempts_zero(self, non_python_fixture): patch('pdd.sync_orchestration.save_run_report'), \ patch('pdd.sync_orchestration._display_sync_log'), \ patch('pdd.sync_orchestration._save_fingerprint_atomic'), \ - patch('pdd.get_test_command.get_test_command_for_file', return_value="go test"), \ - patch('pdd.sync_orchestration.subprocess.run') as mock_subprocess, \ + patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command="go test")), \ + patch('pdd.sync_orchestration._run_fix_operation_test_subprocess') as mock_subprocess, \ patch.object(sys.stdout, 'isatty', return_value=True): self._setup_sync_app_mock(mock_sync_app_class) @@ -3956,9 +4162,9 @@ def capture_subprocess(*args, **kwargs): SyncDecision(operation='all_synced', reason='Done'), ] - with patch('pdd.sync_orchestration.subprocess.run', side_effect=capture_subprocess): + with patch('pdd.sync_orchestration._run_fix_operation_test_subprocess', side_effect=capture_subprocess): with patch('pdd.sync_orchestration.detect_host_python_executable', return_value='python'): - with patch('pdd.get_test_command.get_test_command_for_file', return_value='pytest'): + with patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command='pytest')): sync_orchestration(basename="foo", language="python") # Find pytest calls @@ -5075,9 +5281,9 @@ def test_fix_operation_passes_auto_submit_false_when_local(orchestration_fixture mock_result.stderr = "" mock_result.returncode = 1 - with patch('pdd.sync_orchestration.subprocess.run', return_value=mock_result), \ + with patch('pdd.sync_orchestration._run_fix_operation_test_subprocess', return_value=mock_result), \ patch('pdd.sync_orchestration.detect_host_python_executable', return_value='python'), \ - patch('pdd.get_test_command.get_test_command_for_file', return_value='pytest'): + patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command='pytest')): result = sync_orchestration(basename="calculator", language="python", local=True) fix_main_mock = orchestration_fixture['fix_main'] @@ -5112,9 +5318,9 @@ def test_fix_operation_passes_auto_submit_true_when_not_local(orchestration_fixt mock_result.stderr = "" mock_result.returncode = 1 - with patch('pdd.sync_orchestration.subprocess.run', return_value=mock_result), \ + with patch('pdd.sync_orchestration._run_fix_operation_test_subprocess', return_value=mock_result), \ patch('pdd.sync_orchestration.detect_host_python_executable', return_value='python'), \ - patch('pdd.get_test_command.get_test_command_for_file', return_value='pytest'): + patch('pdd.get_test_command.get_test_command_for_file', return_value=TestCommand(command='pytest')): result = sync_orchestration(basename="calculator", language="python", local=False) fix_main_mock = orchestration_fixture['fix_main'] @@ -7049,8 +7255,11 @@ def test_e2e_typescript_real_execution_detects_failures(tmp_path): # Patch get_test_command_for_file to use Jest from project root # (_execute_tests_and_create_run_report runs from test_file.parent, but Jest # needs the project root where jest.config.js and node_modules live) - jest_cmd = f"cd {tmp_path} && npx jest --no-coverage -- {test_file}" - with patch('pdd.get_test_command.get_test_command_for_file', return_value=jest_cmd): + jest_cmd = f"npx jest --no-coverage -- {test_file}" + with patch( + 'pdd.get_test_command.get_test_command_for_file', + return_value=TestCommand(command=jest_cmd, cwd=tmp_path), + ): # Compare: synthetic report vs real execution synthetic = _create_synthetic_run_report_for_agentic_success(test_file, "calculator", "typescript") real = _execute_tests_and_create_run_report(test_file, "calculator", "typescript", 80.0) diff --git a/tests/test_update_command.py b/tests/test_update_command.py index 4f9828365..a6fb652aa 100644 --- a/tests/test_update_command.py +++ b/tests/test_update_command.py @@ -6,6 +6,125 @@ from pdd.cli import cli +class TestUpdateCommandRepoAll: + """Explicit --all flag matches repository-wide mode (no file arguments).""" + + @patch("pdd.commands.modify.update_main", return_value=("ok", 0.0, "mock")) + def test_all_flag_same_as_no_args(self, mock_update_main, tmp_path): + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + result_none = runner.invoke(cli, ["--force", "update"]) + result_all = runner.invoke(cli, ["--force", "update", "--all"]) + + assert result_none.exit_code == 0, result_none.output + assert result_all.exit_code == 0, result_all.output + assert mock_update_main.call_count == 2 + kw0 = mock_update_main.call_args_list[0].kwargs + kw1 = mock_update_main.call_args_list[1].kwargs + for key in ( + "input_prompt_file", + "modified_code_file", + "input_code_file", + "output", + "use_git", + "repo", + "extensions", + "directory", + "simple", + "base_branch", + "dry_run", + "budget", + ): + assert kw0[key] == kw1[key], f"mismatch on {key}: {kw0[key]!r} vs {kw1[key]!r}" + assert kw0["repo"] is True + + @patch("pdd.commands.modify.update_main", return_value=("ok", 0.0, "mock")) + def test_all_with_extensions_passed_through(self, mock_update_main, tmp_path): + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + r1 = runner.invoke( + cli, ["--force", "update", "--extensions", ".py,.js"] + ) + r2 = runner.invoke( + cli, + ["--force", "update", "--all", "--extensions", ".py,.js"], + ) + assert r1.exit_code == 0 and r2.exit_code == 0 + assert mock_update_main.call_args_list[0].kwargs["extensions"] == ".py,.js" + assert mock_update_main.call_args_list[1].kwargs["extensions"] == ".py,.js" + + def test_all_with_file_arguments_errors(self, tmp_path): + code = tmp_path / "a.py" + code.write_text("x = 1\n") + runner = CliRunner() + result = runner.invoke(cli, ["--force", "update", "--all", str(code)]) + assert result.exit_code != 0 + assert "--all" in result.output and "file" in result.output.lower() + + +class TestUpdateCommandDryRun: + """--dry-run is supported only in repository-wide mode.""" + + @patch("pdd.commands.modify.update_main", return_value=("Dry run: 1 prompt(s) would be updated.", 0.0, "N/A")) + def test_dry_run_passed_for_repo_wide(self, mock_update_main, tmp_path): + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + r = runner.invoke(cli, ["--force", "update", "--dry-run"]) + assert r.exit_code == 0, r.output + mock_update_main.assert_called_once() + assert mock_update_main.call_args.kwargs["repo"] is True + assert mock_update_main.call_args.kwargs["dry_run"] is True + + @patch("pdd.commands.modify.update_main", return_value=("ok", 0.0, "m")) + def test_dry_run_false_by_default(self, mock_update_main, tmp_path): + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + runner.invoke(cli, ["--force", "update"]) + assert mock_update_main.call_args.kwargs["dry_run"] is False + + def test_dry_run_with_file_arguments_errors(self, tmp_path): + code = tmp_path / "a.py" + code.write_text("x = 1\n") + runner = CliRunner() + result = runner.invoke(cli, ["--force", "update", "--dry-run", str(code)]) + assert result.exit_code != 0 + assert "dry" in result.output.lower() and "repository" in result.output.lower() + + +class TestUpdateCommandBudget: + """--budget applies to repository-wide mode update.""" + + @patch("pdd.commands.modify.update_main", return_value=("ok", 0.0, "m")) + def test_budget_passed_for_repo_wide(self, mock_update_main, tmp_path): + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + r = runner.invoke(cli, ["--force", "update", "--budget", "1.25"]) + assert r.exit_code == 0, r.output + assert mock_update_main.call_args.kwargs["repo"] is True + assert mock_update_main.call_args.kwargs["budget"] == pytest.approx(1.25) + + @patch("pdd.commands.modify.update_main", return_value=("ok", 0.0, "m")) + def test_budget_default_none(self, mock_update_main, tmp_path): + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + runner.invoke(cli, ["--force", "update"]) + assert mock_update_main.call_args.kwargs["budget"] is None + + def test_budget_with_file_arguments_errors(self, tmp_path): + code = tmp_path / "a.py" + code.write_text("x = 1\n") + runner = CliRunner() + result = runner.invoke(cli, ["--force", "update", "--budget", "1.0", str(code)]) + assert result.exit_code != 0 + assert "budget" in result.output.lower() and "repository" in result.output.lower() + + def test_budget_must_be_positive(self): + runner = CliRunner() + result = runner.invoke(cli, ["--force", "update", "--budget", "0"]) + assert result.exit_code != 0 + assert "budget" in result.output.lower() and "positive" in result.output.lower() + + class TestUpdateCommandArgs: """Test update command accepts 1, 2, and 3 positional arguments.""" diff --git a/tests/test_update_main.py b/tests/test_update_main.py index 65e39ed8d..10780b7c0 100644 --- a/tests/test_update_main.py +++ b/tests/test_update_main.py @@ -1,12 +1,17 @@ import pytest import sys import os +from pathlib import Path from unittest.mock import patch, MagicMock, mock_open import click from click.testing import CliRunner +import git -# Import the function under test from the pdd package (module named the same as the function). -from pdd.update_main import update_main +from pdd.update_main import ( + _included_docs_for_drift_report, + find_and_resolve_all_pairs, + update_main, +) @pytest.fixture def mock_ctx(): @@ -318,10 +323,54 @@ def test_update_main_handles_unexpected_exception_gracefully( # --- Tests for --repo functionality --- -import os -from pathlib import Path -import git -from pdd.update_main import find_and_resolve_all_pairs + +def test_included_docs_for_drift_report_counts_all_prompts_referencing_doc(tmp_path, monkeypatch): + """Doc rows show how many scan-wide prompts include each doc, not only drifted count.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "README.md").write_text("# hi\n", encoding="utf-8") + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "api.md").write_text("api", encoding="utf-8") + (tmp_path / "prompts").mkdir() + p = tmp_path / "prompts" / "m_python.prompt" + p.write_text( + "../README.md\n../docs/api.md\n", + encoding="utf-8", + ) + p2 = tmp_path / "prompts" / "n_python.prompt" + p2.write_text("../README.md\n", encoding="utf-8") + all_prompts = [str(p), str(p2)] + # Only m_python is drifted; README is still included by 2 prompts in the scan. + agg = _included_docs_for_drift_report(str(tmp_path), all_prompts, [str(p)]) + by_name = {rel: c for rel, c in agg} + assert by_name.get("README.md") == 2 + assert by_name.get("docs/api.md") == 1 + + +def test_estimate_dry_run_cost_range_is_flat_per_pair(tmp_path, monkeypatch): + """Dry-run estimate is a flat $0.50–$1.00 per drifted pair.""" + from pdd.update_main import _estimate_dry_run_cost_range + + repo = git.Repo.init(tmp_path) + (tmp_path / "small.py").write_text("a") + (tmp_path / "prompts").mkdir() + sp = tmp_path / "prompts" / "small_python.prompt" + sp.write_text("prompt") + repo.index.add([str(tmp_path / "small.py"), str(sp)]) + repo.index.commit("init") + monkeypatch.chdir(tmp_path) + + ctx = click.Context(click.Command("update")) + ctx.obj = {} + small_items = [(str(sp), str(tmp_path / "small.py"), "r")] + + lo_s, hi_s = _estimate_dry_run_cost_range(ctx, repo, True, small_items) + assert lo_s == 0.5 + assert hi_s == 1.0 + + lo_0, hi_0 = _estimate_dry_run_cost_range(ctx, repo, True, []) + assert lo_0 == 0.0 + assert hi_0 == 0.0 + @pytest.fixture def mock_get_language_for_repo(monkeypatch): @@ -562,6 +611,91 @@ def mock_update_logic(prompt_file, code_file, ctx, repo, simple=False): assert result[0] == "Repository update complete." +@patch('pdd.architecture_sync.update_architecture_from_prompt', return_value={"success": False, "updated": False, "changes": {}}) +@patch('pdd.update_main.is_code_changed', return_value=(True, "no fingerprint, file in git changed set")) +@patch('pdd.update_main.get_git_changed_files', return_value=set()) +@patch('pdd.update_main.update_file_pair') +def test_update_main_repo_mode_honors_budget_cap(mock_update_file_pair, mock_git_changed, mock_is_changed, mock_arch, temp_git_repo, capsys): + """Repo mode should stop processing new files once budget cap is reached.""" + costs = iter([0.60, 0.60, 0.60]) # 3 changed pairs in fixture + + def mock_update_logic(prompt_file, code_file, ctx, repo, simple=False): + return { + "prompt_file": prompt_file, + "status": "✅ Success", + "cost": next(costs), + "model": "mock_model", + "error": "", + } + + mock_update_file_pair.side_effect = mock_update_logic + + ctx = click.Context(click.Command('update')) + ctx.obj = {"strength": 0.5, "temperature": 0.1, "verbose": False, "time": 0.25, "quiet": False} + + result = update_main( + ctx=ctx, + input_prompt_file=None, + modified_code_file=None, + input_code_file=None, + output=None, + use_git=False, + repo=True, + budget=1.0, + ) + + # First two updates run (0.6 + 0.6), then cap is reached and third is skipped. + assert mock_update_file_pair.call_count == 2 + captured = capsys.readouterr() + assert "budget cap reached" in captured.out.lower() + assert result is not None + assert result[1] == pytest.approx(1.2) + + +@patch("pdd.architecture_sync.update_architecture_from_prompt", return_value={"success": False, "updated": False, "changes": {}}) +@patch("pdd.update_main.is_code_changed", return_value=(True, "no fingerprint, file in git changed set")) +@patch("pdd.update_main.get_git_changed_files", return_value=set()) +@patch("pdd.update_main.update_file_pair") +@patch("pdd.pddrc_initializer.ensure_pddrc_for_scan") +def test_update_main_repo_mode_dry_run_skips_work( + mock_ensure_pddrc, + mock_update_file_pair, + mock_git_changed, + mock_is_changed, + mock_arch, + temp_git_repo, + capsys, +): + """Repository-wide dry run must not call update_file_pair or ensure_pddrc_for_scan.""" + ctx = click.Context(click.Command("update")) + ctx.obj = {"strength": 0.5, "temperature": 0.1, "verbose": False, "time": 0.25, "quiet": False} + + result = update_main( + ctx=ctx, + input_prompt_file=None, + modified_code_file=None, + input_code_file=None, + output=None, + use_git=False, + repo=True, + dry_run=True, + ) + + mock_update_file_pair.assert_not_called() + mock_ensure_pddrc.assert_not_called() + captured = capsys.readouterr() + assert "Repository drift report" in captured.out + assert "Changed files:" in captured.out + assert "Estimated cost:" in captured.out + assert "Drifted modules:" in captured.out + assert "Included docs that may need updating:" in captured.out + assert "Repository Update Summary" not in captured.out + assert result is not None + assert result[1] == 0.0 + assert "would be updated" in result[0].lower() + assert result[2] == "N/A" + + # --- Tests for .pddrc prompts_dir configuration (GitHub Issue #86) --- def test_update_regeneration_mode_respects_pddrc_prompts_dir(tmp_path, monkeypatch):