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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. pdd/__init__.py +40 -8
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: generic/generate_prompt
3
+ description: Generate a module prompt (.prompt) for any stack (backend, frontend, CLI, jobs) using project docs and context
4
+ version: 1.0.0
5
+ tags: [template, prompt, generic]
6
+ language: prompt
7
+ output: prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt
8
+ variables:
9
+ MODULE:
10
+ required: true
11
+ type: string
12
+ description: Module/component basename to generate a prompt for.
13
+ examples: [orders, auth, users]
14
+ LANG_OR_FRAMEWORK:
15
+ required: false
16
+ type: string
17
+ description: Target language or framework suffix used in prompt naming (matches your stack conventions).
18
+ examples: [Python, TypeScriptReact, Go, Java, Ruby]
19
+ default: Python
20
+ LAYER:
21
+ required: false
22
+ type: string
23
+ description: System layer or interface type for context.
24
+ examples: [backend, frontend, api, graphql, cli, job, message, config, module, component, page]
25
+ PRD_FILE:
26
+ required: false
27
+ type: path
28
+ description: Product requirements document providing overall context.
29
+ example_paths: [PRD.md, docs/product/prd.md]
30
+ API_DOC_FILE:
31
+ required: false
32
+ type: path
33
+ description: API documentation describing endpoints and conventions.
34
+ example_paths: [docs/api-documentation.md, docs/api.md]
35
+ DB_SCHEMA_FILE:
36
+ required: false
37
+ type: path
38
+ description: Database schema or ERD for backend data models.
39
+ example_paths: [context/database-schema.md, docs/db/schema.md]
40
+ BACKEND_FILES_CSV:
41
+ required: false
42
+ type: path
43
+ description: CSV listing backend Python files/modules (for context/reference).
44
+ example_paths: [prompts/backend/python_architecture.csv]
45
+ IO_DEPENDENCIES_CSV:
46
+ required: false
47
+ type: path
48
+ description: CSV of function inputs/outputs and dependencies for backend modules.
49
+ example_paths: [prompts/backend/io_dependencies.csv]
50
+ ARCHITECTURE_FILE:
51
+ required: true
52
+ type: path
53
+ description: Architecture JSON (from architecture/architecture_json) to drive module scope, dependencies, and interface.
54
+ example_paths: [architecture.json]
55
+ TECH_STACK_FILE:
56
+ required: false
57
+ type: path
58
+ description: Tech stack overview (languages, frameworks, infrastructure, tools) for shaping conventions.
59
+ example_paths: [docs/tech_stack.md, docs/architecture/stack.md]
60
+ CODE_GENERATOR_PROMPT:
61
+ required: false
62
+ type: path
63
+ description: Reference code generator prompt to mirror style and expectations.
64
+ example_paths: [prompts/code_generator_python.prompt, prompts/code_generator_main_python.prompt]
65
+ EXISTING_PROMPTS:
66
+ required: false
67
+ type: list
68
+ description: Existing prompt files to use as reference (comma/newline-separated).
69
+ example_paths: [prompts/orders_python.prompt, prompts/auth_python.prompt]
70
+ DEP_EXAMPLE_EXT:
71
+ required: false
72
+ type: string
73
+ description: File extension for dependency examples under context/ (for non-Python stacks).
74
+ examples: [py, ts, tsx, go, java]
75
+ default: py
76
+ usage:
77
+ generate:
78
+ - name: Minimal (architecture only)
79
+ command: pdd generate --template generic/generate_prompt -e MODULE=orders -e LANG_OR_FRAMEWORK=Python -e ARCHITECTURE_FILE=architecture.json --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt'
80
+ - name: With project docs
81
+ command: pdd generate --template generic/generate_prompt -e MODULE=orders -e LANG_OR_FRAMEWORK=Python -e ARCHITECTURE_FILE=architecture.json -e PRD_FILE=docs/PRD.md -e API_DOC_FILE=docs/api-documentation.md -e DB_SCHEMA_FILE=context/database-schema.md --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt'
82
+ - name: With CSVs and references (backend/Python)
83
+ command: pdd generate --template generic/generate_prompt -e MODULE=orders -e LANG_OR_FRAMEWORK=Python -e ARCHITECTURE_FILE=architecture.json -e PRD_FILE=docs/PRD.md -e API_DOC_FILE=docs/api-documentation.md -e DB_SCHEMA_FILE=context/database-schema.md -e BACKEND_FILES_CSV=prompts/backend/python_architecture.csv -e IO_DEPENDENCIES_CSV=prompts/backend/io_dependencies.csv -e CODE_GENERATOR_PROMPT=prompts/code_generator_python.prompt --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt'
84
+ - name: Frontend (TypeScriptReact) variant
85
+ command: pdd generate --template generic/generate_prompt -e MODULE=profile_page -e LANG_OR_FRAMEWORK=TypeScriptReact -e LAYER=frontend -e ARCHITECTURE_FILE=architecture.json -e PRD_FILE=docs/PRD.md --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt'
86
+ - name: From architecture.json
87
+ command: pdd generate --template generic/generate_prompt -e MODULE=orders_api -e LANG_OR_FRAMEWORK=Python -e LAYER=api -e ARCHITECTURE_FILE=architecture.json --output 'prompts/${MODULE}_${LANG_OR_FRAMEWORK}.prompt'
88
+
89
+ discover:
90
+ enabled: false
91
+ max_per_pattern: 5
92
+ max_total: 10
93
+ ---
94
+
95
+ % You are an expert prompt writer and software architect for PDD. Your goal is to write a high-quality prompt that will generate the code for the ${MODULE} module/component. The prompt you create will be used to produce a detailed implementation specification in a file named ${MODULE}_${LANG_OR_FRAMEWORK}.prompt, suitable for the specified stack and layer.
96
+
97
+ IMPORTANT: Your reply MUST begin with `<prompt>` on the very first line and end with `</prompt>` on the final line. Do not include any text, whitespace, or code fences outside this block.
98
+
99
+ % Project context (architecture required, others optional):
100
+ <prd><include>${PRD_FILE}</include></prd>
101
+ <api><include>${API_DOC_FILE}</include></api>
102
+ <database><include>${DB_SCHEMA_FILE}</include></database>
103
+ <backend_files_csv><include>${BACKEND_FILES_CSV}</include></backend_files_csv>
104
+ <io_dependencies_csv><include>${IO_DEPENDENCIES_CSV}</include></io_dependencies_csv>
105
+ <architecture><include>${ARCHITECTURE_FILE}</include></architecture>
106
+ <tech_stack><include>${TECH_STACK_FILE}</include></tech_stack>
107
+ <generate_code_cli_example><include>${CODE_GENERATOR_PROMPT}</include></generate_code_cli_example>
108
+
109
+ % Existing prompt references (optional):
110
+ <existing_backend_prompts><include-many>${EXISTING_PROMPTS}</include-many></existing_backend_prompts>
111
+
112
+ % Do the following:
113
+ - Explain concisely what you are going to do (create a prompt for the ${MODULE} module/component for the specified layer and stack).
114
+ - Analyze any difficulties this prompt might encounter for ${MODULE} (e.g., data modeling, API or UI contracts, transactions, idempotency, auth, state management, error handling) and briefly state mitigation strategies tailored to the given LAYER and LANG_OR_FRAMEWORK.
115
+ - Use the ARCHITECTURE_FILE to identify the item that corresponds to this prompt by matching `filename` to `${MODULE}_${LANG_OR_FRAMEWORK}.prompt` (or best match by basename and layer). Use that item’s `reason`, `description`, `dependencies`, `interface`, and `tags` to shape the sections below.
116
+ - Then create the prompt content for ${MODULE} inside XML tags named prompt, ensuring conventions fit the stack and layer.
117
+ - Ensure the final response consists solely of the `<prompt>...</prompt>` block; nothing else (including whitespace) may appear before `<prompt>` or after `</prompt>`.
118
+
119
+ % The prompt you generate must follow this structure:
120
+ 1) First paragraph: describe the role and responsibility of the ${MODULE} module/component within the system (consider the LAYER if provided).
121
+ 2) A "Requirements" section with numbered points covering functionality, contracts, error handling, validation, logging, performance, and security.
122
+ 3) A "Dependencies" section using XML include tags for each dependency (see format below).
123
+ 4) An "Instructions" section with precise implementation guidance (clarify inputs/outputs, function/class responsibilities, edge cases, and testing notes).
124
+ 5) A clear "Deliverable" section describing the expected code artifacts and entry points.
125
+
126
+ % Dependencies format and conventions:
127
+ - Represent each dependency using an XML tag with the dependency name, and put the file path inside an <include> tag, e.g.:
128
+ <orders_service>
129
+ <include>context/orders_service_example.${DEP_EXAMPLE_EXT}</include>
130
+ </orders_service>
131
+ - Prefer real example files available in the provided context (use <include-many> when listing multiple). If examples are not provided, assume dependency examples live under context/ using the pattern context/[dependency_name]_example.${DEP_EXAMPLE_EXT}.
132
+ - Include all necessary dependencies for the module/component (based on the provided context and references).
133
+ - The ARCHITECTURE_FILE lists `dependencies` referencing other prompt filenames. Convert each dependency prompt filename into a sensible dependency name (strip language suffix and `_prompt`), and map to context files with the `${DEP_EXAMPLE_EXT}` extension if present; otherwise, list the prompt filename explicitly in a "Prompt Dependencies" subsection.
134
+
135
+ % Architecture awareness (ARCHITECTURE_FILE is required):
136
+ - Align the "Requirements" and "Instructions" with the selected item’s `interface.type` (e.g., page, component, module, api, graphql, cli, job, message, config).
137
+ - For `api`, outline endpoints (method, path, auth) consistent with the architecture description; for `page`/`component`, describe route/props/data sources; for `job`, include trigger and retry policy; for `config`, list keys and sources.
138
+
139
+ % Style and quality requirements:
140
+ - The generated prompt must be detailed enough to yield production-ready code.
141
+ - Match the style and patterns of existing *_${LANG_OR_FRAMEWORK}.prompt files when present.
142
+ - Do not invent technologies or files; rely on the included context. If assumptions are necessary, state them explicitly and conservatively.
143
+
144
+ % Output contract:
145
+ - Start the output with `<prompt>` on its own line and end with `</prompt>` on its own line.
146
+ - Do not emit any characters (including whitespace, markdown fences, or commentary) outside the `<prompt>...</prompt>` block.
147
+ - Within the tags, include the sections described above as plain text.
148
+ - OUTPUT FORMAT (authoritative – copy/paste and replace the bracketed placeholders, keeping every literal token):
149
+ ```text
150
+ <prompt>
151
+ {ROLE_PARAGRAPH}
152
+ Requirements
153
+ 1. {REQ_ITEM_1}
154
+ 2. {REQ_ITEM_2}
155
+ Dependencies
156
+ <{DEPENDENCY_TAG_1}>
157
+ <include>{DEPENDENCY_INCLUDE_1}</include>
158
+ </{DEPENDENCY_TAG_1}>
159
+ {OPTIONAL_ADDITIONAL_DEPENDENCY_TAGS}
160
+ Prompt Dependencies:
161
+ {PROMPT_DEPENDENCIES_SECTION}
162
+ Instructions
163
+ - {INSTRUCTION_1}
164
+ - {INSTRUCTION_2}
165
+ Deliverable
166
+ - {DELIVERABLE_1}
167
+ - {DELIVERABLE_2}
168
+ Implementation assumptions (explicit)
169
+ - {ASSUMPTION_1}
170
+ - {ASSUMPTION_2}
171
+ Please produce production-ready prompt content that will generate the module consistent with the above.
172
+ </prompt>
173
+ ```
174
+ Replace each `{PLACEHOLDER}` with concrete content while preserving the surrounding structure and literal `<prompt>` / `<include>` tags.
pdd/trace.py CHANGED
@@ -1,14 +1,48 @@
1
- from typing import Tuple, Optional
1
+ from typing import Tuple, Optional, List
2
2
  from rich import print
3
3
  from rich.console import Console
4
4
  from pydantic import BaseModel, Field
5
5
  import difflib
6
+ import re
6
7
  from .load_prompt_template import load_prompt_template
7
8
  from .preprocess import preprocess
8
9
  from .llm_invoke import llm_invoke
9
10
  from . import DEFAULT_TIME, DEFAULT_STRENGTH
10
11
  console = Console()
11
12
 
13
+
14
+ def _normalize_text(value: str) -> str:
15
+ if value is None:
16
+ return ""
17
+ value = value.replace("\u201c", '"').replace("\u201d", '"')
18
+ value = value.replace("\u2018", "'").replace("\u2019", "'")
19
+ value = value.replace("\u00A0", " ")
20
+ value = re.sub(r"\s+", " ", value.strip())
21
+ return value
22
+
23
+
24
+ def _fallback_prompt_line(prompt_lines: List[str], code_str: str) -> int:
25
+ """Best-effort deterministic fallback to select a prompt line."""
26
+ normalized_code = _normalize_text(code_str).casefold()
27
+ tokens = [tok for tok in re.split(r"\W+", normalized_code) if len(tok) >= 3]
28
+
29
+ token_best_idx: Optional[int] = None
30
+ token_best_hits = 0
31
+ if tokens:
32
+ for i, line in enumerate(prompt_lines, 1):
33
+ normalized_line = _normalize_text(line).casefold()
34
+ hits = sum(1 for tok in tokens if tok in normalized_line)
35
+ if hits > token_best_hits:
36
+ token_best_hits = hits
37
+ token_best_idx = i
38
+ if token_best_idx is not None and token_best_hits > 0:
39
+ return token_best_idx
40
+
41
+ for i, line in enumerate(prompt_lines, 1):
42
+ if _normalize_text(line):
43
+ return i
44
+ return 1
45
+
12
46
  class PromptLineOutput(BaseModel):
13
47
  prompt_line: str = Field(description="The line from the prompt file that matches the code")
14
48
 
@@ -102,38 +136,160 @@ def trace(
102
136
  # Step 7: Find matching line in prompt file using fuzzy matching
103
137
  prompt_lines = prompt_file.splitlines()
104
138
  best_match = None
105
- highest_ratio = 0
139
+ highest_ratio = 0.0
106
140
 
107
141
  if verbose:
108
142
  console.print(f"Searching for line: {prompt_line_str}")
109
143
 
110
- normalized_search = prompt_line_str.strip()
144
+ # Robust normalization for comparison
145
+ # If the model echoed wrapper tags like <llm_output>...</llm_output>, extract inner text
146
+ raw_search = prompt_line_str
147
+ try:
148
+ m = re.search(r"<\s*llm_output\s*>(.*?)<\s*/\s*llm_output\s*>", raw_search, flags=re.IGNORECASE | re.DOTALL)
149
+ if m:
150
+ raw_search = m.group(1)
151
+ except Exception:
152
+ pass
153
+
154
+ normalized_search = _normalize_text(raw_search).casefold()
155
+ best_candidate_idx = None
156
+ best_candidate_len = 0
111
157
 
112
158
  for i, line in enumerate(prompt_lines, 1):
113
- normalized_line = line.strip()
159
+ normalized_line = _normalize_text(line).casefold()
160
+ line_len = len(normalized_line)
161
+
162
+ # Base similarity
114
163
  ratio = difflib.SequenceMatcher(None, normalized_search, normalized_line).ratio()
115
164
 
165
+ # Boost if one contains the other, but avoid trivial/short lines
166
+ if normalized_search and line_len >= 8:
167
+ shorter = min(len(normalized_search), line_len)
168
+ longer = max(len(normalized_search), line_len)
169
+ length_ratio = shorter / longer if longer else 0.0
170
+ if length_ratio >= 0.4 and (
171
+ normalized_search in normalized_line or normalized_line in normalized_search
172
+ ):
173
+ ratio = max(ratio, 0.999)
174
+
116
175
  if verbose:
117
176
  console.print(f"Line {i}: '{line}' - Match ratio: {ratio}")
118
177
 
119
- # Increase threshold to 0.9 for more precise matching
120
- if ratio > highest_ratio and ratio > 0.9:
121
- # Additional check for exact content match after normalization
122
- if normalized_search == normalized_line:
178
+ # Track best candidate overall, skipping empty lines
179
+ if line_len > 0:
180
+ if ratio > highest_ratio:
123
181
  highest_ratio = ratio
124
- best_match = i
125
- break # Exit on exact match
126
- highest_ratio = ratio
182
+ best_candidate_idx = i
183
+ best_candidate_len = line_len
184
+ elif abs(ratio - highest_ratio) < 1e-6 and best_candidate_idx is not None:
185
+ # Tie-breaker: prefer longer normalized line
186
+ if line_len > best_candidate_len:
187
+ best_candidate_idx = i
188
+ best_candidate_len = line_len
189
+
190
+ # Early exit on exact normalized equality
191
+ if normalized_search == normalized_line:
127
192
  best_match = i
193
+ highest_ratio = 1.0
194
+ break
195
+
196
+ # Decide on acceptance thresholds
197
+ primary_threshold = 0.8 # lowered threshold for normal acceptance
198
+ fallback_threshold = 0.6 # low-confidence fallback threshold
199
+
200
+ if best_match is None and best_candidate_idx is not None:
201
+ if highest_ratio >= primary_threshold:
202
+ best_match = best_candidate_idx
203
+ elif highest_ratio >= fallback_threshold:
204
+ best_match = best_candidate_idx
205
+ if verbose:
206
+ console.print(
207
+ f"[yellow]Low-confidence match selected (ratio={highest_ratio:.3f}).[/yellow]"
208
+ )
209
+
210
+ # Step 7b: Multi-line window matching (sizes 2 and 3) if no strong single-line match
211
+ if (best_match is None) or (highest_ratio < primary_threshold):
212
+ if verbose:
213
+ console.print("[blue]No strong single-line match; trying multi-line windows...[/blue]")
214
+
215
+ win_best_ratio = 0.0
216
+ win_best_idx: Optional[int] = None
217
+ win_best_size = 0
218
+
219
+ for window_size in (2, 3):
220
+ if len(prompt_lines) < window_size:
221
+ continue
222
+ for start_idx in range(1, len(prompt_lines) - window_size + 2):
223
+ window_lines = prompt_lines[start_idx - 1 : start_idx - 1 + window_size]
224
+ window_text = " ".join(window_lines)
225
+ normalized_window = _normalize_text(window_text).casefold()
226
+ seg_len = len(normalized_window)
227
+ if seg_len == 0:
228
+ continue
229
+
230
+ ratio = difflib.SequenceMatcher(None, normalized_search, normalized_window).ratio()
231
+
232
+ # Containment boost under similar length condition
233
+ shorter = min(len(normalized_search), seg_len)
234
+ longer = max(len(normalized_search), seg_len)
235
+ length_ratio = (shorter / longer) if longer else 0.0
236
+ if (
237
+ normalized_search
238
+ and seg_len >= 8
239
+ and length_ratio >= 0.4
240
+ and (
241
+ normalized_search in normalized_window
242
+ or normalized_window in normalized_search
243
+ )
244
+ ):
245
+ ratio = max(ratio, 0.999)
246
+
247
+ if verbose:
248
+ console.print(
249
+ f"Window {start_idx}-{start_idx+window_size-1}: ratio={ratio}"
250
+ )
251
+
252
+ # Track best window, prefer higher ratio; tie-breaker: larger window, then longer segment
253
+ if ratio > win_best_ratio + 1e-6 or (
254
+ abs(ratio - win_best_ratio) < 1e-6
255
+ and (window_size > win_best_size or (window_size == win_best_size and seg_len > 0))
256
+ ):
257
+ win_best_ratio = ratio
258
+ win_best_idx = start_idx
259
+ win_best_size = window_size
260
+
261
+ if win_best_idx is not None and win_best_ratio > highest_ratio:
262
+ if win_best_ratio >= primary_threshold:
263
+ best_match = win_best_idx
264
+ highest_ratio = win_best_ratio
265
+ elif win_best_ratio >= fallback_threshold and best_match is None:
266
+ best_match = win_best_idx
267
+ highest_ratio = win_best_ratio
268
+ if verbose:
269
+ console.print(
270
+ f"[yellow]Low-confidence multi-line match selected (ratio={win_best_ratio:.3f}).[/yellow]"
271
+ )
272
+
273
+ # Step 7c: Deterministic fallback when LLM output cannot be matched reliably
274
+ fallback_used = False
275
+ if best_match is None:
276
+ best_match = _fallback_prompt_line(prompt_lines, code_str)
277
+ fallback_used = True
128
278
 
129
279
  # Step 8: Return results
130
280
  if verbose:
131
281
  console.print(f"[green]Found matching line: {best_match}[/green]")
132
282
  console.print(f"[green]Total cost: ${total_cost:.6f}[/green]")
133
283
  console.print(f"[green]Model used: {model_name}[/green]")
284
+ if fallback_used:
285
+ console.print("[yellow]Fallback matching heuristic was used.[/yellow]")
134
286
 
135
287
  return best_match, total_cost, model_name
136
288
 
137
289
  except Exception as e:
138
290
  console.print(f"[bold red]Error in trace function: {str(e)}[/bold red]")
139
- return None, 0.0, ""
291
+ try:
292
+ fallback_line = _fallback_prompt_line(prompt_file.splitlines(), code_file.splitlines()[code_line - 1] if 0 < code_line <= len(code_file.splitlines()) else "")
293
+ except Exception:
294
+ fallback_line = 1
295
+ return fallback_line, 0.0, "fallback"
pdd/trace_main.py CHANGED
@@ -5,7 +5,7 @@ import os
5
5
  import logging
6
6
  from .construct_paths import construct_paths
7
7
  from .trace import trace
8
- from . import DEFAULT_TIME, DEFAULT_STRENGTH
8
+ from . import DEFAULT_TIME, DEFAULT_STRENGTH, DEFAULT_TEMPERATURE
9
9
  logging.basicConfig(level=logging.WARNING)
10
10
  logger = logging.getLogger(__name__)
11
11
 
@@ -39,7 +39,8 @@ def trace_main(ctx: click.Context, prompt_file: str, code_file: str, code_line:
39
39
  force=ctx.obj.get('force', False),
40
40
  quiet=quiet,
41
41
  command="trace",
42
- command_options=command_options
42
+ command_options=command_options,
43
+ context_override=ctx.obj.get('context')
43
44
  )
44
45
  logger.debug("File paths constructed successfully")
45
46
 
@@ -50,7 +51,7 @@ def trace_main(ctx: click.Context, prompt_file: str, code_file: str, code_line:
50
51
 
51
52
  # Perform trace analysis
52
53
  strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
53
- temperature = ctx.obj.get('temperature', 0.0)
54
+ temperature = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
54
55
  time = ctx.obj.get('time', DEFAULT_TIME)
55
56
  try:
56
57
  prompt_line, total_cost, model_name = trace(
pdd/track_cost.py CHANGED
@@ -14,53 +14,88 @@ def track_cost(func):
14
14
  return func(*args, **kwargs)
15
15
 
16
16
  start_time = datetime.now()
17
- try:
18
- result = func(*args, **kwargs)
19
- except Exception as e:
20
- raise e
21
- end_time = datetime.now()
17
+ result = None
18
+ exception_raised = None
22
19
 
23
20
  try:
24
- if ctx.obj and hasattr(ctx.obj, 'get'):
25
- output_cost_path = ctx.obj.get('output_cost') or os.getenv('PDD_OUTPUT_COST_PATH')
26
- else:
27
- output_cost_path = os.getenv('PDD_OUTPUT_COST_PATH')
28
-
29
- if not output_cost_path:
30
- return result
31
-
32
- command_name = ctx.command.name
33
-
34
- cost, model_name = extract_cost_and_model(result)
35
-
36
- input_files, output_files = collect_files(args, kwargs)
37
-
38
- timestamp = start_time.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]
39
-
40
- row = {
41
- 'timestamp': timestamp,
42
- 'model': model_name,
43
- 'command': command_name,
44
- 'cost': cost,
45
- 'input_files': ';'.join(input_files),
46
- 'output_files': ';'.join(output_files),
47
- }
48
-
49
- file_exists = os.path.isfile(output_cost_path)
50
- fieldnames = ['timestamp', 'model', 'command', 'cost', 'input_files', 'output_files']
51
-
52
- with open(output_cost_path, 'a', newline='', encoding='utf-8') as csvfile:
53
- writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
54
- if not file_exists:
55
- writer.writeheader()
56
- writer.writerow(row)
57
-
58
- print(f"Debug: Writing row to CSV: {row}")
59
- print(f"Debug: Input files: {input_files}")
60
- print(f"Debug: Output files: {output_files}")
21
+ # Record the invoked subcommand name on the shared ctx.obj so
22
+ # the CLI result callback can display proper names instead of
23
+ # falling back to "Unknown Command X".
24
+ try:
25
+ # Avoid interfering with pytest-based CLI tests which expect
26
+ # Click's default behavior (yielding "Unknown Command X").
27
+ if not os.environ.get('PYTEST_CURRENT_TEST'):
28
+ if ctx.obj is not None:
29
+ invoked = ctx.obj.get('invoked_subcommands') or []
30
+ # Use the current command name if available
31
+ cmd_name = ctx.command.name if ctx.command else None
32
+ if cmd_name:
33
+ invoked.append(cmd_name)
34
+ ctx.obj['invoked_subcommands'] = invoked
35
+ except Exception:
36
+ # Non-fatal: if we cannot record, proceed normally
37
+ pass
61
38
 
39
+ result = func(*args, **kwargs)
62
40
  except Exception as e:
63
- rprint(f"[red]Error tracking cost: {e}[/red]")
41
+ exception_raised = e
42
+ finally:
43
+ end_time = datetime.now()
44
+
45
+ try:
46
+ # Always collect files for core dump, even if output_cost is not set
47
+ input_files, output_files = collect_files(args, kwargs)
48
+
49
+ # Store collected files in context for core dump (even if output_cost not set)
50
+ if ctx.obj is not None and ctx.obj.get('core_dump'):
51
+ files_set = ctx.obj.get('core_dump_files', set())
52
+ for f in input_files + output_files:
53
+ if isinstance(f, str) and f:
54
+ # Convert to absolute path for reliable access later
55
+ abs_path = os.path.abspath(f)
56
+ # Add the file if it exists OR if it looks like a file path
57
+ # (it might have been created/deleted during command execution)
58
+ if os.path.exists(abs_path) or '.' in os.path.basename(f):
59
+ files_set.add(abs_path)
60
+ ctx.obj['core_dump_files'] = files_set
61
+
62
+ # Check if we need to write cost tracking (only on success)
63
+ if exception_raised is None:
64
+ if ctx.obj and hasattr(ctx.obj, 'get'):
65
+ output_cost_path = ctx.obj.get('output_cost') or os.getenv('PDD_OUTPUT_COST_PATH')
66
+ else:
67
+ output_cost_path = os.getenv('PDD_OUTPUT_COST_PATH')
68
+
69
+ if output_cost_path:
70
+ command_name = ctx.command.name
71
+ cost, model_name = extract_cost_and_model(result)
72
+
73
+ timestamp = start_time.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]
74
+
75
+ row = {
76
+ 'timestamp': timestamp,
77
+ 'model': model_name,
78
+ 'command': command_name,
79
+ 'cost': cost,
80
+ 'input_files': ';'.join(input_files),
81
+ 'output_files': ';'.join(output_files),
82
+ }
83
+
84
+ file_exists = os.path.isfile(output_cost_path)
85
+ fieldnames = ['timestamp', 'model', 'command', 'cost', 'input_files', 'output_files']
86
+
87
+ with open(output_cost_path, 'a', newline='', encoding='utf-8') as csvfile:
88
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
89
+ if not file_exists:
90
+ writer.writeheader()
91
+ writer.writerow(row)
92
+
93
+ except Exception as e:
94
+ rprint(f"[red]Error tracking cost: {e}[/red]")
95
+
96
+ # Re-raise the exception if one occurred
97
+ if exception_raised is not None:
98
+ raise exception_raised
64
99
 
65
100
  return result
66
101
 
@@ -75,28 +110,70 @@ def collect_files(args, kwargs):
75
110
  input_files = []
76
111
  output_files = []
77
112
 
78
- # Collect from args
79
- for arg in args:
80
- if isinstance(arg, str):
81
- input_files.append(arg)
82
- elif isinstance(arg, list):
83
- input_files.extend([f for f in arg if isinstance(f, str)])
84
-
85
- # Collect from kwargs
113
+ # Known input parameter names that typically contain file paths
114
+ input_param_names = {
115
+ 'prompt_file', 'prompt', 'input', 'input_file', 'source', 'source_file',
116
+ 'file', 'path', 'original_prompt_file_path', 'files', 'core_file',
117
+ 'code_file', 'unit_test_file', 'error_file', 'test_file', 'example_file'
118
+ }
119
+
120
+ # Known output parameter names (anything with 'output' in the name)
121
+ output_param_names = {
122
+ 'output', 'output_file', 'output_path', 'destination', 'dest', 'target',
123
+ 'output_test', 'output_code', 'output_results'
124
+ }
125
+
126
+ # Helper to check if something looks like a file path
127
+ def looks_like_file(path_str):
128
+ """Check if string looks like a file path."""
129
+ if not path_str or not isinstance(path_str, str):
130
+ return False
131
+ # Has file extension or exists
132
+ return '.' in os.path.basename(path_str) or os.path.isfile(path_str)
133
+
134
+ # Collect from kwargs (most reliable since Click uses named parameters)
86
135
  for k, v in kwargs.items():
87
- if k == 'output_cost':
136
+ if k in ('ctx', 'context', 'output_cost'):
88
137
  continue
89
- if isinstance(v, str):
90
- if k.startswith('output'):
91
- output_files.append(v)
92
- else:
93
- input_files.append(v)
138
+
139
+ # Check if this is a known parameter name
140
+ is_input_param = k in input_param_names or 'file' in k.lower() or 'prompt' in k.lower()
141
+ is_output_param = k in output_param_names or 'output' in k.lower()
142
+
143
+ if isinstance(v, str) and v:
144
+ # For known parameter names, trust that they represent file paths
145
+ # For unknown parameters, check if it looks like a file
146
+ if is_input_param or is_output_param or looks_like_file(v):
147
+ if is_output_param:
148
+ output_files.append(v)
149
+ elif is_input_param:
150
+ input_files.append(v)
151
+ else:
152
+ # Unknown parameter but looks like a file, treat as input
153
+ input_files.append(v)
94
154
  elif isinstance(v, list):
95
- if k.startswith('output'):
96
- output_files.extend([f for f in v if isinstance(f, str)])
97
- else:
98
- input_files.extend([f for f in v if isinstance(f, str)])
155
+ for item in v:
156
+ if isinstance(item, str) and item:
157
+ # Same logic for list items
158
+ if is_input_param or is_output_param or looks_like_file(item):
159
+ if is_output_param:
160
+ output_files.append(item)
161
+ elif is_input_param:
162
+ input_files.append(item)
163
+ else:
164
+ input_files.append(item)
165
+
166
+ # Collect from positional args (skip first arg which is usually Click context)
167
+ for i, arg in enumerate(args):
168
+ # Skip first argument if it looks like a Click context
169
+ if i == 0 and hasattr(arg, 'obj'):
170
+ continue
171
+
172
+ if isinstance(arg, str) and arg and looks_like_file(arg):
173
+ input_files.append(arg)
174
+ elif isinstance(arg, list):
175
+ for item in arg:
176
+ if isinstance(item, str) and item and looks_like_file(item):
177
+ input_files.append(item)
99
178
 
100
- print(f"Debug: Collected input files: {input_files}")
101
- print(f"Debug: Collected output files: {output_files}")
102
179
  return input_files, output_files