teddy-cli 0.1.0__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 (143) hide show
  1. teddy_cli-0.1.0.dist-info/LICENSE +677 -0
  2. teddy_cli-0.1.0.dist-info/METADATA +33 -0
  3. teddy_cli-0.1.0.dist-info/RECORD +143 -0
  4. teddy_cli-0.1.0.dist-info/WHEEL +4 -0
  5. teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
  6. teddy_executor/__init__.py +1 -0
  7. teddy_executor/__main__.py +335 -0
  8. teddy_executor/adapters/__init__.py +0 -0
  9. teddy_executor/adapters/inbound/__init__.py +0 -0
  10. teddy_executor/adapters/inbound/cli_formatter.py +107 -0
  11. teddy_executor/adapters/inbound/cli_helpers.py +249 -0
  12. teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
  13. teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
  14. teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
  15. teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
  16. teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
  17. teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
  18. teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
  19. teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
  20. teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
  21. teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
  22. teddy_executor/adapters/outbound/__init__.py +7 -0
  23. teddy_executor/adapters/outbound/console_interactor.py +212 -0
  24. teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
  25. teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
  26. teddy_executor/adapters/outbound/console_tooling.py +62 -0
  27. teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
  28. teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
  29. teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
  30. teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
  31. teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
  32. teddy_executor/adapters/outbound/shell_adapter.py +344 -0
  33. teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
  34. teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
  35. teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
  36. teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
  37. teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
  38. teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
  39. teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
  40. teddy_executor/container.py +333 -0
  41. teddy_executor/core/__init__.py +0 -0
  42. teddy_executor/core/domain/__init__.py +0 -0
  43. teddy_executor/core/domain/models/__init__.py +44 -0
  44. teddy_executor/core/domain/models/action_ports.py +28 -0
  45. teddy_executor/core/domain/models/change_set.py +10 -0
  46. teddy_executor/core/domain/models/exceptions.py +40 -0
  47. teddy_executor/core/domain/models/execution_report.py +65 -0
  48. teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
  49. teddy_executor/core/domain/models/plan.py +85 -0
  50. teddy_executor/core/domain/models/planning_ports.py +43 -0
  51. teddy_executor/core/domain/models/project_context.py +56 -0
  52. teddy_executor/core/domain/models/report_assembly_data.py +18 -0
  53. teddy_executor/core/domain/models/session.py +17 -0
  54. teddy_executor/core/domain/models/shell_output.py +12 -0
  55. teddy_executor/core/domain/models/web_search_results.py +26 -0
  56. teddy_executor/core/ports/__init__.py +0 -0
  57. teddy_executor/core/ports/inbound/__init__.py +0 -0
  58. teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
  59. teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
  60. teddy_executor/core/ports/inbound/init.py +15 -0
  61. teddy_executor/core/ports/inbound/plan_parser.py +52 -0
  62. teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
  63. teddy_executor/core/ports/inbound/plan_validator.py +26 -0
  64. teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
  65. teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
  66. teddy_executor/core/ports/outbound/__init__.py +34 -0
  67. teddy_executor/core/ports/outbound/config_service.py +29 -0
  68. teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
  69. teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
  70. teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
  71. teddy_executor/core/ports/outbound/llm_client.py +90 -0
  72. teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
  73. teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
  74. teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
  75. teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
  76. teddy_executor/core/ports/outbound/session_manager.py +97 -0
  77. teddy_executor/core/ports/outbound/session_repository.py +65 -0
  78. teddy_executor/core/ports/outbound/shell_executor.py +24 -0
  79. teddy_executor/core/ports/outbound/system_environment.py +25 -0
  80. teddy_executor/core/ports/outbound/time_service.py +28 -0
  81. teddy_executor/core/ports/outbound/user_interactor.py +126 -0
  82. teddy_executor/core/ports/outbound/web_scraper.py +24 -0
  83. teddy_executor/core/ports/outbound/web_searcher.py +25 -0
  84. teddy_executor/core/services/__init__.py +0 -0
  85. teddy_executor/core/services/action_changeset_builder.py +90 -0
  86. teddy_executor/core/services/action_diff_manager.py +110 -0
  87. teddy_executor/core/services/action_dispatcher.py +142 -0
  88. teddy_executor/core/services/action_executor.py +209 -0
  89. teddy_executor/core/services/action_factory.py +197 -0
  90. teddy_executor/core/services/action_parser_complex.py +216 -0
  91. teddy_executor/core/services/action_parser_strategies.py +84 -0
  92. teddy_executor/core/services/context_service.py +437 -0
  93. teddy_executor/core/services/edit_simulator.py +128 -0
  94. teddy_executor/core/services/execution_orchestrator.py +295 -0
  95. teddy_executor/core/services/execution_report_assembler.py +62 -0
  96. teddy_executor/core/services/init_service.py +80 -0
  97. teddy_executor/core/services/markdown_plan_parser.py +309 -0
  98. teddy_executor/core/services/markdown_report_formatter.py +143 -0
  99. teddy_executor/core/services/parser_infrastructure.py +222 -0
  100. teddy_executor/core/services/parser_metadata.py +153 -0
  101. teddy_executor/core/services/parser_reporting.py +267 -0
  102. teddy_executor/core/services/plan_validator.py +82 -0
  103. teddy_executor/core/services/planning_service.py +242 -0
  104. teddy_executor/core/services/prompt_manager.py +146 -0
  105. teddy_executor/core/services/session_lifecycle_manager.py +228 -0
  106. teddy_executor/core/services/session_loop_guard.py +46 -0
  107. teddy_executor/core/services/session_orchestrator.py +538 -0
  108. teddy_executor/core/services/session_planner.py +43 -0
  109. teddy_executor/core/services/session_pruning_service.py +438 -0
  110. teddy_executor/core/services/session_replanner.py +105 -0
  111. teddy_executor/core/services/session_repository.py +194 -0
  112. teddy_executor/core/services/session_service.py +529 -0
  113. teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
  114. teddy_executor/core/services/validation_rules/__init__.py +4 -0
  115. teddy_executor/core/services/validation_rules/edit.py +207 -0
  116. teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
  117. teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
  118. teddy_executor/core/services/validation_rules/execute.py +37 -0
  119. teddy_executor/core/services/validation_rules/filesystem.py +73 -0
  120. teddy_executor/core/services/validation_rules/helpers.py +178 -0
  121. teddy_executor/core/services/validation_rules/message.py +29 -0
  122. teddy_executor/core/utils/__init__.py +1 -0
  123. teddy_executor/core/utils/diff.py +57 -0
  124. teddy_executor/core/utils/io.py +75 -0
  125. teddy_executor/core/utils/markdown.py +131 -0
  126. teddy_executor/core/utils/serialization.py +39 -0
  127. teddy_executor/core/utils/string.py +351 -0
  128. teddy_executor/prompts.py +45 -0
  129. teddy_executor/registries/__init__.py +1 -0
  130. teddy_executor/registries/infrastructure.py +147 -0
  131. teddy_executor/registries/reviewer.py +57 -0
  132. teddy_executor/registries/validators.py +47 -0
  133. teddy_executor/resources/__init__.py +1 -0
  134. teddy_executor/resources/config/.gitignore +2 -0
  135. teddy_executor/resources/config/__init__.py +1 -0
  136. teddy_executor/resources/config/config.yaml +49 -0
  137. teddy_executor/resources/config/init.context +5 -0
  138. teddy_executor/resources/config/prompts/architect.xml +462 -0
  139. teddy_executor/resources/config/prompts/assistant.xml +336 -0
  140. teddy_executor/resources/config/prompts/debugger.xml +456 -0
  141. teddy_executor/resources/config/prompts/developer.xml +481 -0
  142. teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
  143. teddy_executor/resources/config/prompts/prototyper.xml +425 -0
@@ -0,0 +1,290 @@
1
+ {#
2
+ Unified template for the concise Markdown report.
3
+ Renders both execution outcomes and pre-flight validation failures.
4
+
5
+ NOTE ON WHITESPACE CONTROL:
6
+ This template uses manual whitespace control (`-%}`) on blocks that are
7
+ followed by blank lines. The Jinja2 `trim_blocks=True` setting only
8
+ removes the *first* newline after a tag, not subsequent blank lines.
9
+ The manual control is necessary to prevent extra newlines in the final
10
+ rendered output when conditional sections are not rendered.
11
+ #}
12
+ {% macro render_resource(path, content) %}
13
+ {% if path.startswith('http:') or path.startswith('https:') %}
14
+ ### [{{ path }}]({{ path }})
15
+ {% else %}
16
+ ### [{{ path }}]({% if not path.startswith('/') %}/{% endif %}{{ path }})
17
+ {% endif %}
18
+ {{ content | fence }}{{ path | language_from_path }}
19
+ {{ content }}
20
+ {{ content | fence }}
21
+ {% endmacro %}
22
+
23
+ {% macro render_action_details(log) %}
24
+ {# Handle MESSAGE actions first: render content in a fenced codeblock #}
25
+ {% if log.action_type.upper() == 'MESSAGE' and log.details %}
26
+ {% if log.details is mapping and log.details.get('content') %}
27
+ - **User Reply:**
28
+ {{ log.details.content | trim | fence }}
29
+ {{ log.details.content | trim }}
30
+ {{ log.details.content | trim | fence }}
31
+ {% else %}
32
+ - **User Reply:**
33
+ {{ log.details | trim | fence }}
34
+ {{ log.details | trim }}
35
+ {{ log.details | trim | fence }}
36
+ {% endif %}
37
+
38
+ {% elif log.details -%}
39
+ {% if log.details is mapping -%}
40
+ {% if log.details.get("error") -%}
41
+ - **Error:** {{ log.details.error }}
42
+ {% endif -%}
43
+ {% if log.details.get("original_details") -%}
44
+ - **Error:** {{ log.details.original_details }}
45
+ {% endif -%}
46
+ {% if log.details.get("return_code") is not none -%}
47
+ - **Return Code:** `{{ log.details.return_code }}`
48
+ {% endif -%}
49
+ {% if log.details.get("failed_command") -%}
50
+ - **Failed Command:** `{{ log.details.failed_command }}`
51
+ {% endif -%}
52
+ {% if log.details.get("similarity_scores") -%}
53
+ {% set scores_list = log.details.get("similarity_scores") -%}
54
+ {% if scores_list | length == 1 -%}
55
+ - **Similarity Score:** {{ "%.2f" | format(scores_list[0]) }}
56
+ {% else -%}
57
+ - **Similarity Scores:** {% for s in scores_list %}{{ "%.2f" | format(s) }}{% if not loop.last %}, {% endif %}{% endfor %}
58
+ {% endif %}
59
+
60
+ {% elif log.details.get("similarity_score") is not none -%}
61
+ - **Similarity Score:** {{ "%.2f" | format(log.details.get("similarity_score")) }}
62
+
63
+ {% endif -%}
64
+ {% if log.details.get("stdout") -%}
65
+ #### `stdout`
66
+ {{ log.details.stdout | trim | fence }}text
67
+ {{ log.details.stdout | trim }}
68
+ {{ log.details.stdout | trim | fence }}
69
+
70
+ {% endif -%}
71
+ {% if log.details.get("stderr") -%}
72
+ #### `stderr`
73
+ {{ log.details.stderr | trim | fence }}text
74
+ {{ log.details.stderr | trim }}
75
+ {{ log.details.stderr | trim | fence }}
76
+
77
+ {% endif -%}
78
+ {% if log.details.get("diff") -%}
79
+ #### `diff`
80
+ {{ log.details.diff | trim | fence }}diff
81
+ {{ log.details.diff | trim }}
82
+ {{ log.details.diff | trim | fence }}
83
+
84
+ {% endif -%}
85
+ {# Render generic details if no specific fields matched, but avoid rendering if it's just content #}
86
+ {% if not log.details.get("error") and not log.details.get("return_code") and not log.details.get("stdout") and not log.details.get("stderr") and not log.details.get("content") and not log.details.get("diff") and not log.details.get("failed_command") and not log.details.get("similarity_scores") and not log.details.get("similarity_score") and not log.details.get("query_results") %}
87
+ - **Details:** `{{ log.details }}`
88
+ {% endif -%}
89
+ {% else -%}
90
+ - **Details:** `{{ log.details }}`
91
+ {% endif -%}
92
+ {% endif %}
93
+ {% endmacro -%}
94
+
95
+ {% macro render_header(report) -%}
96
+ # Execution Report: {{ report.plan_title | default('Untitled Plan') }}
97
+ {% set status_val = report.run_summary.status.value | default(report.run_summary.status) -%}
98
+ - **Overall Status:** {{ "Validation Failed" if status_val == "VALIDATION_FAILED" else status_val }}
99
+ - **Execution Start Time:** {{ format_datetime(report.run_summary.start_time) }}
100
+ - **Execution End Time:** {{ format_datetime(report.run_summary.end_time) }}
101
+ {%- endmacro %}
102
+
103
+
104
+ {{ render_header(report) }}
105
+
106
+ {% if report.validation_result -%}
107
+ ## Validation Errors:
108
+ {% for error in report.validation_result -%}
109
+ {{ error | trim }}
110
+ {% if not loop.last %}
111
+
112
+ ---
113
+
114
+ {% endif %}
115
+ {% endfor %}
116
+ {% endif -%}
117
+
118
+ {% if report.validation_ast -%}
119
+ {{ report.validation_ast }}
120
+ {% endif -%}
121
+ {% if report.failed_resources and not is_session %}
122
+
123
+ ## Resource Contents
124
+ {% for path, content in report.failed_resources.items() %}
125
+ {{ render_resource(path, content) }}
126
+ {% endfor %}
127
+ {%- endif %}
128
+ {% if report.user_request %}
129
+
130
+ ## User Request
131
+ {{ report.user_request | fence }}text
132
+ {{ report.user_request }}
133
+ {{ report.user_request | fence }}
134
+ {% endif %}
135
+
136
+ {# Extract logs that should display resource content #}
137
+ {% set report_ns = namespace(content_logs=[]) %}
138
+ {% for log in report.action_logs %}
139
+ {% if log.details and log.details is mapping and log.details.get('content') %}
140
+ {# Always include content for failed actions or successful READ actions (unless in a session). #}
141
+ {% set is_read = log.action_type.lower() == 'read' %}
142
+ {% set is_success = log.status.value == 'SUCCESS' %}
143
+ {% set is_failure = log.status.value == 'FAILURE' %}
144
+
145
+ {% if is_failure or (is_read and is_success) %}
146
+ {% set report_ns.content_logs = report_ns.content_logs + [log] %}
147
+ {% endif %}
148
+ {% endif %}
149
+ {% endfor -%}
150
+
151
+ {% if report_ns.content_logs and not is_session %}
152
+ ## Resource Contents
153
+ {% for log in report_ns.content_logs %}
154
+ {% set res_path = log.params.get('File Path') or log.params.get('Resource') or log.params.get('resource') or log.params.get('path') %}
155
+ {{ render_resource(res_path, log.details.content) }}
156
+ {% endfor %}
157
+ {% endif -%}
158
+ ## Action Log
159
+ {% for log in report.action_logs %}
160
+ {% set action_type = log.action_type.upper() %}
161
+ {% set params = log.params or {} %}
162
+ {% set target = '' %}
163
+
164
+ {# Determine Header Target #}
165
+ {% if action_type == 'EXECUTE' %}
166
+ {% set target = '"' ~ (params.get('description') or params.get('Description') or 'Execute Command') ~ '"' %}
167
+ {% elif action_type == 'RESEARCH' %}
168
+ {% set target = params.get('description') or params.get('Description') or 'Research' %}
169
+ {% else %}
170
+ {# Use Markdown Link for CREATE/EDIT/READ #}
171
+ {% set full_path = params.get('File Path') or params.get('file_path') or params.get('Resource') or params.get('resource') or params.get('path') or '' %}
172
+ {# Check for URL schemes http and https #}
173
+ {% if full_path.startswith('http:') or full_path.startswith('https:') %}
174
+ {% set target = '[' ~ full_path ~ '](' ~ full_path ~ ')' %}
175
+ {% else %}
176
+ {% set target = '[' ~ (full_path | basename) ~ '](' ~ ('/' ~ full_path if not full_path.startswith('/') else full_path) ~ ')' %}
177
+ {% endif %}
178
+ {% endif %}
179
+
180
+ {% if log.modified_fields -%}
181
+ {% set modified_tag = " (user modified: " ~ (log.modified_fields | join(', ')) ~ ")" -%}
182
+ {% elif log.modified -%}
183
+ {% set modified_tag = " (modified)" -%}
184
+ {% else -%}
185
+ {% set modified_tag = "" -%}
186
+ {% endif -%}
187
+ {% if target %}
188
+ ### `{{ action_type }}`{{ modified_tag }}: {{ target }}
189
+ {% else -%}
190
+ ### `{{ action_type }}`{{ modified_tag }}
191
+ {% endif -%}
192
+ {% set log_status = log.status.value | default(log.status) -%}
193
+ - **Status:** {{ log_status }}
194
+ {% if action_type == 'READ' and not params.get('lines') %}
195
+ - **Note:** Up-to-date contents are provided under `Resource Contents`.
196
+ {% endif %}
197
+ {% if action_type in ['CREATE', 'EDIT'] -%}
198
+ {% set path_val = params.get('File Path') or params.get('file_path') or params.get('path') -%}
199
+ {% if path_val -%}
200
+ - **File Path:** `{{ path_val }}`
201
+ {%- endif %}
202
+ {%- endif %}
203
+ {# Params Rendering - Flattened #}
204
+ {% if action_type == 'EXECUTE' %}
205
+ {% set outcome = params.get('expected_outcome') or params.get('Expected Outcome') %}
206
+ {% if outcome %}
207
+ - **Expected outcome:** {{ outcome }}
208
+ {% endif %}
209
+
210
+ {% set setup = params.get('setup') or params.get('Setup') %}
211
+ {% if setup %}
212
+ - **Setup:** `{{ setup }}`
213
+ {% endif %}
214
+
215
+ {% set allow_failure = params.get('allow_failure') or params.get('Allow Failure') %}
216
+ {% if allow_failure is not none %}
217
+ - **Allow Failure:** `{{ allow_failure | lower }}`
218
+ {% endif %}
219
+
220
+ {% set cmd = params.get('command') or params.get('cmd') %}
221
+ {% if cmd %}
222
+
223
+ - **Command:** {% if '\n' in cmd %}
224
+ {{ '\n' }}{{ cmd | fence }}shell
225
+ {{ cmd }}
226
+ {{ cmd | fence }}
227
+ {% else %}`{{ cmd }}`{% endif %}
228
+ {% endif %}
229
+
230
+ {% else %}
231
+ {# Generic Params for CREATE, EDIT, READ, RESEARCH etc #}
232
+ {% if params %}
233
+ {# Use namespace for persistent ignored_keys across if-blocks #}
234
+ {% set param_ns = namespace(ignored=['content', 'find', 'replace', 'edits', 'similarity_scores', 'similarity_score', 'similarity threshold', 'similarity_threshold', 'metadata used file path alias']) %}
235
+
236
+ {% if action_type in ['EXECUTE', 'RESEARCH'] %}
237
+ {% set param_ns.ignored = param_ns.ignored + ['description', 'Description'] %}
238
+ {% endif %}
239
+ {% if action_type == 'RESEARCH' %}
240
+ {% set param_ns.ignored = param_ns.ignored + ['queries'] %}
241
+ {% endif %}
242
+
243
+ {% for key, value in params.items() %}
244
+ {% set k_lower = key.lower() %}
245
+ {% if k_lower not in param_ns.ignored %}
246
+ {# Normalize key to Title Case #}
247
+ {% set display_key = key.replace('_', ' ').title() %}
248
+ {# Map plumbing keys back to human readable names for display #}
249
+ {% if k_lower == 'similarity_threshold' %}{% set display_key = 'Similarity Threshold' %}{% endif %}
250
+ {% if k_lower == 'match_all' %}{% set display_key = 'Match All' %}{% endif %}
251
+ {# Skip path params in body since they are in header #}
252
+ {% if k_lower not in ['file path', 'file_path', 'resource', 'path'] %}
253
+ - **{{ display_key }}:** {{ value }}
254
+ {% endif %}
255
+ {% endif %}
256
+ {% endfor %}
257
+ {% endif %}
258
+ {% endif %}
259
+
260
+ {% if action_type == 'RESEARCH' and log.details and log.details is mapping and log.details.get('query_results') %}
261
+ {% for qr in log.details.get('query_results', []) %}
262
+
263
+ #### Query: "{{ qr['query'] }}"
264
+ {% for sr in qr['results'] %}
265
+ {{ loop.index }}. `{{ sr['href'] }}`
266
+ - **Title:** {{ sr['title'] }}
267
+ - **Description:** {{ sr['description'] }}
268
+ {% endfor %}
269
+ {% endfor %}
270
+
271
+ _**Hint:** NOW you MUST use READ on the most promising results)_
272
+ {% endif %}
273
+
274
+ {% if action_type == 'READ' and log.details and log.details is mapping and log.details.get('content') and params.get('lines') %}
275
+ {{ log.details.content | trim | fence }}
276
+ {{ log.details.content | trim }}
277
+ {{ log.details.content | trim | fence }}
278
+ {% endif %}
279
+
280
+ {{ render_action_details(log) }}
281
+ {% else %}
282
+ *(No actions were executed.)*
283
+ {% endfor %}
284
+ {% if report.validation_result %}
285
+
286
+ ## Validation Recovery
287
+ 1. Identify the root cause of the failure based on your system instructions (MRP) & context.
288
+ 2. Submit a corrected plan focusing strictly on the failed/problematic action(s).
289
+ 3. Defer the remaining steps of the original plan to subsequent turns.
290
+ {% endif %}
@@ -0,0 +1,4 @@
1
+ """
2
+ This package contains the specific validation rules for different action types,
3
+ refactored from the main PlanValidator.
4
+ """
@@ -0,0 +1,207 @@
1
+ """
2
+ Validation rules for the 'EDIT' action.
3
+ """
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ from teddy_executor.core.domain.models.plan import (
9
+ ActionData,
10
+ DEFAULT_SIMILARITY_THRESHOLD,
11
+ )
12
+ from teddy_executor.core.ports.outbound import IConfigService, IFileSystemManager
13
+ from teddy_executor.core.services.validation_rules.edit_matcher import (
14
+ find_best_match_and_diff,
15
+ )
16
+ from teddy_executor.core.services.validation_rules.helpers import (
17
+ BaseActionValidator,
18
+ ContextPaths,
19
+ PlanValidationError,
20
+ ValidationError,
21
+ ValidationResult,
22
+ resolve_similarity_threshold,
23
+ validate_path_is_safe,
24
+ )
25
+ from teddy_executor.core.utils.markdown import get_fence_for_content
26
+
27
+
28
+ class EditActionValidator(BaseActionValidator):
29
+ """Validator for the 'EDIT' action."""
30
+
31
+ def __init__(
32
+ self, file_system_manager: IFileSystemManager, config_service: IConfigService
33
+ ):
34
+ super().__init__(file_system_manager)
35
+ self._config_service = config_service
36
+
37
+ def validate(
38
+ self,
39
+ action: ActionData,
40
+ context_paths: Optional[ContextPaths] = None,
41
+ ) -> ValidationResult:
42
+ """
43
+ Validates an 'edit' action.
44
+ """
45
+ path_str = (
46
+ action.params.get("path")
47
+ or action.params.get("file_path")
48
+ or action.params.get("File Path")
49
+ or action.params.get("path")
50
+ )
51
+
52
+ if not isinstance(path_str, str):
53
+ return []
54
+
55
+ try:
56
+ validate_path_is_safe(path_str, "EDIT")
57
+
58
+ # Context Check: Removed as per 02-04-Context Automation slice.
59
+ # EDIT actions are now allowed on any valid path, relying on the matcher.
60
+
61
+ if not self._file_system_manager.path_exists(path_str):
62
+ raise PlanValidationError(
63
+ f"File to edit does not exist: {path_str}",
64
+ file_path=path_str,
65
+ )
66
+
67
+ except (PlanValidationError, FileNotFoundError) as e:
68
+ return [
69
+ ValidationError(
70
+ message=getattr(e, "message", str(e)),
71
+ file_path=getattr(e, "file_path", path_str),
72
+ offending_node=action.node,
73
+ )
74
+ ]
75
+
76
+ action_errors: ValidationResult = []
77
+ content = self._file_system_manager.read_raw_file(path_str)
78
+
79
+ # Use centralized similarity threshold resolution
80
+ threshold = resolve_similarity_threshold(self._config_service, action.params)
81
+
82
+ match_all = action.params.get("match_all", False)
83
+ edits = action.params.get("edits")
84
+ if isinstance(edits, list):
85
+ for edit in edits:
86
+ # Local override from edit metadata takes precedence
87
+ local_match_all = edit.get("match_all", match_all)
88
+ for err in _validate_single_edit(
89
+ edit, content, path_str, threshold, match_all=local_match_all
90
+ ):
91
+ # Attach specific FIND CodeBlock node for surgical diagnostics
92
+ # Fallback to action node if find_node is missing
93
+ offending_node = edit.get("find_node") or action.node
94
+ action_errors.append(
95
+ ValidationError(
96
+ message=err.message,
97
+ file_path=err.file_path,
98
+ offending_node=offending_node,
99
+ )
100
+ )
101
+ return action_errors
102
+
103
+
104
+ def _validate_single_edit(
105
+ edit: dict,
106
+ content: str,
107
+ file_path: str,
108
+ threshold: Optional[float] = None,
109
+ match_all: bool = False,
110
+ ) -> ValidationResult:
111
+ """Validates a single edit dictionary."""
112
+ errors: ValidationResult = []
113
+ find_block = edit.get("find")
114
+ replace_block = edit.get("replace")
115
+
116
+ if os.environ.get("TEDDY_DEBUG"):
117
+ print("\n--- TEDDY DEBUG: PlanValidator ---")
118
+ print(f"File: {file_path}")
119
+ print(f"Content (repr): {repr(content)}")
120
+ print(f"Find Block (repr): {repr(find_block)}")
121
+ print("--- END TEDDY DEBUG ---\n")
122
+
123
+ if isinstance(find_block, str):
124
+ if find_block == replace_block:
125
+ # Treat identical blocks as a successful no-op (02-04-Context Automation slice)
126
+ return []
127
+
128
+ # Use the resilient matcher for all matching logic
129
+ matcher_kwargs = {}
130
+ if threshold is not None:
131
+ matcher_kwargs["threshold"] = threshold
132
+
133
+ diff_text, score, is_ambiguous, offset = find_best_match_and_diff(
134
+ content, find_block, **matcher_kwargs
135
+ )
136
+
137
+ effective_threshold = (
138
+ threshold if threshold is not None else DEFAULT_SIMILARITY_THRESHOLD
139
+ )
140
+ fence = get_fence_for_content(find_block)
141
+
142
+ if is_ambiguous and not match_all:
143
+ errors.append(
144
+ ValidationError(
145
+ message=(
146
+ f"The `FIND` block is ambiguous in: {file_path}\n"
147
+ f"**Similarity Score:** {score:.2f}\n"
148
+ f"**FIND Block:**\n"
149
+ f"{fence}\n{find_block}\n{fence}\n"
150
+ "**Hint:** Please provide a larger FIND block to uniquely identify the section, refactor the code to avoid duplication. Alternatively you can use `Match All: true` to change all occurrences in the file at once."
151
+ ),
152
+ file_path=str(file_path),
153
+ )
154
+ )
155
+ elif score < effective_threshold:
156
+ error_msg = (
157
+ f"The `FIND` block could not be located in the file: "
158
+ f"{file_path}\n"
159
+ f"**Similarity Score:** {score:.2f}\n"
160
+ f"**Similarity Threshold:** {effective_threshold:.2f}\n"
161
+ f"**FIND Block:**\n"
162
+ f"{fence}\n{find_block}\n{fence}\n"
163
+ )
164
+ if diff_text:
165
+ # Prepend standard diff headers for high-clarity diagnostics
166
+ # ndiff(find, actual) means '-' is Provided and '+' is Actual
167
+ formatted_diff = f"--- Provided\n+++ Actual\n{diff_text}"
168
+ diff_fence = get_fence_for_content(formatted_diff)
169
+ error_msg += (
170
+ f"**Closest Match Diff:**\n{diff_fence}diff\n"
171
+ f"{formatted_diff}\n{diff_fence}\n"
172
+ )
173
+
174
+ hint = _get_already_applied_hint(
175
+ content, replace_block, effective_threshold, matcher_kwargs
176
+ )
177
+ error_msg += f"**Hint:** {hint}"
178
+ errors.append(ValidationError(message=error_msg, file_path=str(file_path)))
179
+ return errors
180
+
181
+
182
+ def _get_already_applied_hint(
183
+ content: str,
184
+ replace_block: Optional[str],
185
+ threshold: float,
186
+ matcher_kwargs: dict,
187
+ ) -> str:
188
+ """Detects if the REPLACE block is already present in the content."""
189
+ replace_score = 0.0
190
+ if isinstance(replace_block, str):
191
+ _, replace_score, _, _ = find_best_match_and_diff(
192
+ content, replace_block, **matcher_kwargs
193
+ )
194
+
195
+ if replace_score >= threshold:
196
+ return (
197
+ "The FIND block was not found, but the REPLACE block is already "
198
+ "present. This change might have already been applied."
199
+ )
200
+
201
+ return (
202
+ "Review the provided diff and make sure to match the target content "
203
+ "exactly, including whitespace and indentations."
204
+ )
205
+
206
+
207
+ # Removed legacy functional validation rule in favor of EditActionValidator class.