diamond-dev 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.
- diamond_dev/__init__.py +1 -0
- diamond_dev/acceptance.py +79 -0
- diamond_dev/agents.py +186 -0
- diamond_dev/commands.py +294 -0
- diamond_dev/comparison_bundle.py +345 -0
- diamond_dev/config.py +793 -0
- diamond_dev/config_init.py +266 -0
- diamond_dev/errors.py +41 -0
- diamond_dev/executor.py +382 -0
- diamond_dev/git_ops.py +423 -0
- diamond_dev/logging_setup.py +209 -0
- diamond_dev/main.py +83 -0
- diamond_dev/markdown.py +10 -0
- diamond_dev/naming.py +123 -0
- diamond_dev/notify.py +46 -0
- diamond_dev/orchestrator.py +1169 -0
- diamond_dev/orchestrator_repositories.py +245 -0
- diamond_dev/pr.py +160 -0
- diamond_dev/preflight.py +68 -0
- diamond_dev/providers.py +131 -0
- diamond_dev/report.py +236 -0
- diamond_dev/review_judgments.py +239 -0
- diamond_dev/workflow.py +283 -0
- diamond_dev-0.1.0.dist-info/METADATA +518 -0
- diamond_dev-0.1.0.dist-info/RECORD +28 -0
- diamond_dev-0.1.0.dist-info/WHEEL +4 -0
- diamond_dev-0.1.0.dist-info/entry_points.txt +3 -0
- diamond_dev-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Deterministic comparison bundle generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from diamond_dev.executor import CommandRunner
|
|
15
|
+
from diamond_dev.git_ops import GitOperations
|
|
16
|
+
from diamond_dev.workflow import ImplementationBranch, RunContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class _DiffBudget:
|
|
21
|
+
remaining_bytes: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class _ChangedFile:
|
|
26
|
+
status: str
|
|
27
|
+
display_path: str
|
|
28
|
+
diff_path: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_comparison_bundle(
|
|
32
|
+
*,
|
|
33
|
+
context: RunContext,
|
|
34
|
+
runner: CommandRunner,
|
|
35
|
+
git: GitOperations,
|
|
36
|
+
) -> RunContext:
|
|
37
|
+
"""Write the deterministic comparison bundle and return updated context."""
|
|
38
|
+
diff_budget = _DiffBudget(
|
|
39
|
+
remaining_bytes=context.config.comparison.max_total_diff_bytes,
|
|
40
|
+
)
|
|
41
|
+
active_context = context
|
|
42
|
+
lines = [
|
|
43
|
+
"# Diamond Dev comparison bundle",
|
|
44
|
+
"",
|
|
45
|
+
f"- Plan: {context.plan.file_name}",
|
|
46
|
+
f"- Base branch: {context.implementation.base_branch}",
|
|
47
|
+
f"- Diff byte budget: {context.config.comparison.max_total_diff_bytes}",
|
|
48
|
+
f"- Per-file diff byte cap: {context.config.comparison.max_file_diff_bytes}",
|
|
49
|
+
"",
|
|
50
|
+
]
|
|
51
|
+
for branch in context.implementation.branches:
|
|
52
|
+
branch_lines, tests_ran = _branch_section(
|
|
53
|
+
context=context,
|
|
54
|
+
runner=runner,
|
|
55
|
+
git=git,
|
|
56
|
+
branch=branch,
|
|
57
|
+
diff_budget=diff_budget,
|
|
58
|
+
)
|
|
59
|
+
lines.extend(branch_lines)
|
|
60
|
+
if tests_ran:
|
|
61
|
+
active_context = git.record_dirty_files(
|
|
62
|
+
active_context,
|
|
63
|
+
f"{branch.agent_name} comparison tests",
|
|
64
|
+
branch.repo_dir,
|
|
65
|
+
branch.branch,
|
|
66
|
+
log_prefix=f"{branch.log_prefix}-comparison-tests",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
bundle_markdown = "\n".join(lines).rstrip()
|
|
70
|
+
context.comparison_bundle_file.write_text(f"{bundle_markdown}\n", encoding="utf-8")
|
|
71
|
+
logger.info("Wrote comparison bundle: {}", context.comparison_bundle_file)
|
|
72
|
+
return active_context
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _branch_section(
|
|
76
|
+
*,
|
|
77
|
+
context: RunContext,
|
|
78
|
+
runner: CommandRunner,
|
|
79
|
+
git: GitOperations,
|
|
80
|
+
branch: ImplementationBranch,
|
|
81
|
+
diff_budget: _DiffBudget,
|
|
82
|
+
) -> tuple[list[str], bool]:
|
|
83
|
+
head_revision = git.revision(
|
|
84
|
+
branch.repo_dir,
|
|
85
|
+
branch.branch,
|
|
86
|
+
log_name=f"{branch.log_prefix}-comparison-head-revision",
|
|
87
|
+
)
|
|
88
|
+
ahead_behind = git.branch_ahead_behind(
|
|
89
|
+
branch.repo_dir,
|
|
90
|
+
branch=branch.branch,
|
|
91
|
+
base_branch=context.implementation.base_branch,
|
|
92
|
+
log_name=f"{branch.log_prefix}-comparison-ahead-behind",
|
|
93
|
+
)
|
|
94
|
+
changed_files = _changed_files(
|
|
95
|
+
git.run(
|
|
96
|
+
branch.repo_dir,
|
|
97
|
+
"diff",
|
|
98
|
+
"--name-status",
|
|
99
|
+
f"origin/{context.implementation.base_branch}...{branch.branch}",
|
|
100
|
+
log_name=f"{branch.log_prefix}-comparison-name-status",
|
|
101
|
+
).output,
|
|
102
|
+
)
|
|
103
|
+
lines = [
|
|
104
|
+
f"## {branch.agent_name}",
|
|
105
|
+
"",
|
|
106
|
+
f"- Branch: {branch.branch}",
|
|
107
|
+
f"- Repository: {branch.repo_dir}",
|
|
108
|
+
f"- Head SHA: {head_revision}",
|
|
109
|
+
(
|
|
110
|
+
"- Ahead/behind base: "
|
|
111
|
+
f"ahead={ahead_behind.ahead}, behind={ahead_behind.behind}"
|
|
112
|
+
),
|
|
113
|
+
f"- Changed files: {len(changed_files)}",
|
|
114
|
+
*_change_stat_lines(changed_files),
|
|
115
|
+
"",
|
|
116
|
+
"### Changed file list",
|
|
117
|
+
"",
|
|
118
|
+
*_changed_file_list_lines(
|
|
119
|
+
changed_files,
|
|
120
|
+
context.config.comparison.max_file_diff_bytes,
|
|
121
|
+
),
|
|
122
|
+
"",
|
|
123
|
+
"### Tests",
|
|
124
|
+
"",
|
|
125
|
+
]
|
|
126
|
+
test_lines, tests_ran = _test_lines(
|
|
127
|
+
context=context,
|
|
128
|
+
runner=runner,
|
|
129
|
+
branch=branch,
|
|
130
|
+
)
|
|
131
|
+
lines.extend(test_lines)
|
|
132
|
+
lines.extend(("", "### Capped diffs", ""))
|
|
133
|
+
lines.extend(
|
|
134
|
+
_diff_lines(
|
|
135
|
+
context=context,
|
|
136
|
+
git=git,
|
|
137
|
+
branch=branch,
|
|
138
|
+
changed_files=changed_files,
|
|
139
|
+
diff_budget=diff_budget,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
lines.append("")
|
|
143
|
+
return lines, tests_ran
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _changed_files(name_status_output: str) -> tuple[_ChangedFile, ...]:
|
|
147
|
+
files: list[_ChangedFile] = []
|
|
148
|
+
for line in name_status_output.splitlines():
|
|
149
|
+
if not line.strip():
|
|
150
|
+
continue
|
|
151
|
+
parts = line.split("\t")
|
|
152
|
+
status = parts[0]
|
|
153
|
+
if len(parts) == 1:
|
|
154
|
+
display_path = parts[0]
|
|
155
|
+
diff_path = parts[0]
|
|
156
|
+
elif status.startswith(("R", "C")) and len(parts) >= 3:
|
|
157
|
+
display_path = f"{parts[-2]} -> {parts[-1]}"
|
|
158
|
+
diff_path = parts[-1]
|
|
159
|
+
else:
|
|
160
|
+
display_path = parts[-1]
|
|
161
|
+
diff_path = parts[-1]
|
|
162
|
+
files.append(
|
|
163
|
+
_ChangedFile(
|
|
164
|
+
status=status,
|
|
165
|
+
display_path=display_path,
|
|
166
|
+
diff_path=diff_path,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
return tuple(files)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _change_stat_lines(changed_files: Sequence[_ChangedFile]) -> list[str]:
|
|
173
|
+
stats = {
|
|
174
|
+
"added": 0,
|
|
175
|
+
"modified": 0,
|
|
176
|
+
"deleted": 0,
|
|
177
|
+
"renamed": 0,
|
|
178
|
+
"copied": 0,
|
|
179
|
+
"other": 0,
|
|
180
|
+
}
|
|
181
|
+
for changed_file in changed_files:
|
|
182
|
+
match changed_file.status[:1]:
|
|
183
|
+
case "A":
|
|
184
|
+
stats["added"] += 1
|
|
185
|
+
case "M":
|
|
186
|
+
stats["modified"] += 1
|
|
187
|
+
case "D":
|
|
188
|
+
stats["deleted"] += 1
|
|
189
|
+
case "R":
|
|
190
|
+
stats["renamed"] += 1
|
|
191
|
+
case "C":
|
|
192
|
+
stats["copied"] += 1
|
|
193
|
+
case _:
|
|
194
|
+
stats["other"] += 1
|
|
195
|
+
summary = ", ".join(f"{key}={value}" for key, value in stats.items() if value)
|
|
196
|
+
return [f"- Change stats: {summary or 'none'}"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _changed_file_list_lines(
|
|
200
|
+
changed_files: Sequence[_ChangedFile],
|
|
201
|
+
byte_budget: int,
|
|
202
|
+
) -> list[str]:
|
|
203
|
+
if not changed_files:
|
|
204
|
+
return ["- No changed files."]
|
|
205
|
+
|
|
206
|
+
included, omitted = _capped_lines(
|
|
207
|
+
(
|
|
208
|
+
f"- {changed_file.status}: {changed_file.display_path}"
|
|
209
|
+
for changed_file in changed_files
|
|
210
|
+
),
|
|
211
|
+
max_bytes=byte_budget,
|
|
212
|
+
)
|
|
213
|
+
if not included:
|
|
214
|
+
included = ["- All changed files omitted due to byte budget."]
|
|
215
|
+
if omitted:
|
|
216
|
+
included.extend(("", "Omitted changed files:", *omitted))
|
|
217
|
+
return included
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _test_lines(
|
|
221
|
+
*,
|
|
222
|
+
context: RunContext,
|
|
223
|
+
runner: CommandRunner,
|
|
224
|
+
branch: ImplementationBranch,
|
|
225
|
+
) -> tuple[list[str], bool]:
|
|
226
|
+
if not context.config.comparison.test_commands:
|
|
227
|
+
return ["- tests: not_run"], False
|
|
228
|
+
|
|
229
|
+
lines: list[str] = []
|
|
230
|
+
for index, command_text in enumerate(
|
|
231
|
+
context.config.comparison.test_commands,
|
|
232
|
+
start=1,
|
|
233
|
+
):
|
|
234
|
+
log_name = f"{branch.log_prefix}-comparison-test-{index}"
|
|
235
|
+
result = runner.run(
|
|
236
|
+
("sh", "-lc", command_text),
|
|
237
|
+
cwd=branch.repo_dir,
|
|
238
|
+
log_name=log_name,
|
|
239
|
+
check=False,
|
|
240
|
+
)
|
|
241
|
+
status = "passed" if result.returncode == 0 else "failed"
|
|
242
|
+
clipped_output, omitted_bytes = _clip_bytes(
|
|
243
|
+
result.output,
|
|
244
|
+
context.config.comparison.max_test_output_bytes,
|
|
245
|
+
)
|
|
246
|
+
lines.extend(
|
|
247
|
+
(
|
|
248
|
+
f"- Command {index}: `{command_text}`",
|
|
249
|
+
f" - Status: {status} (exit {result.returncode})",
|
|
250
|
+
f" - Log: {result.log_path}",
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
if clipped_output:
|
|
254
|
+
lines.extend((" - Output:", " ```text"))
|
|
255
|
+
lines.extend(f" {line}" for line in clipped_output.splitlines())
|
|
256
|
+
lines.append(" ```")
|
|
257
|
+
if omitted_bytes:
|
|
258
|
+
lines.append(f" - Omitted output bytes: {omitted_bytes}")
|
|
259
|
+
return lines, True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _diff_lines(
|
|
263
|
+
*,
|
|
264
|
+
context: RunContext,
|
|
265
|
+
git: GitOperations,
|
|
266
|
+
branch: ImplementationBranch,
|
|
267
|
+
changed_files: Sequence[_ChangedFile],
|
|
268
|
+
diff_budget: _DiffBudget,
|
|
269
|
+
) -> list[str]:
|
|
270
|
+
lines: list[str] = []
|
|
271
|
+
omitted: list[str] = []
|
|
272
|
+
for index, changed_file in enumerate(changed_files, start=1):
|
|
273
|
+
if diff_budget.remaining_bytes <= 0:
|
|
274
|
+
omitted.append(
|
|
275
|
+
f"- {changed_file.display_path}: total diff budget exhausted",
|
|
276
|
+
)
|
|
277
|
+
continue
|
|
278
|
+
result = git.run(
|
|
279
|
+
branch.repo_dir,
|
|
280
|
+
"diff",
|
|
281
|
+
"--no-color",
|
|
282
|
+
f"origin/{context.implementation.base_branch}...{branch.branch}",
|
|
283
|
+
"--",
|
|
284
|
+
changed_file.diff_path,
|
|
285
|
+
log_name=f"{branch.log_prefix}-comparison-diff-{index}",
|
|
286
|
+
)
|
|
287
|
+
diff_text, file_omitted = _clip_bytes(
|
|
288
|
+
result.output,
|
|
289
|
+
context.config.comparison.max_file_diff_bytes,
|
|
290
|
+
)
|
|
291
|
+
diff_text, total_omitted = _clip_bytes(diff_text, diff_budget.remaining_bytes)
|
|
292
|
+
diff_budget.remaining_bytes -= len(diff_text.encode("utf-8"))
|
|
293
|
+
if not diff_text.strip():
|
|
294
|
+
continue
|
|
295
|
+
lines.extend(
|
|
296
|
+
(
|
|
297
|
+
f"#### {changed_file.display_path}",
|
|
298
|
+
"",
|
|
299
|
+
"```diff",
|
|
300
|
+
diff_text.rstrip(),
|
|
301
|
+
"```",
|
|
302
|
+
"",
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
if file_omitted:
|
|
306
|
+
omitted.append(
|
|
307
|
+
f"- {changed_file.display_path}: omitted {file_omitted} bytes "
|
|
308
|
+
"by per-file cap",
|
|
309
|
+
)
|
|
310
|
+
if total_omitted:
|
|
311
|
+
omitted.append(
|
|
312
|
+
f"- {changed_file.display_path}: omitted {total_omitted} bytes "
|
|
313
|
+
"by total diff cap",
|
|
314
|
+
)
|
|
315
|
+
if not lines:
|
|
316
|
+
lines.append("- No diff content included.")
|
|
317
|
+
if omitted:
|
|
318
|
+
lines.extend(("### Omitted diff files", "", *omitted))
|
|
319
|
+
return lines
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _capped_lines(
|
|
323
|
+
lines: Iterable[str],
|
|
324
|
+
*,
|
|
325
|
+
max_bytes: int,
|
|
326
|
+
) -> tuple[list[str], list[str]]:
|
|
327
|
+
included: list[str] = []
|
|
328
|
+
omitted: list[str] = []
|
|
329
|
+
used_bytes = 0
|
|
330
|
+
for line in lines:
|
|
331
|
+
line_bytes = len(f"{line}\n".encode("utf-8")) # noqa: UP012
|
|
332
|
+
if used_bytes + line_bytes > max_bytes:
|
|
333
|
+
omitted.append(line)
|
|
334
|
+
continue
|
|
335
|
+
included.append(line)
|
|
336
|
+
used_bytes += line_bytes
|
|
337
|
+
return included, omitted
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _clip_bytes(text: str, max_bytes: int) -> tuple[str, int]:
|
|
341
|
+
encoded = text.encode("utf-8")
|
|
342
|
+
if len(encoded) <= max_bytes:
|
|
343
|
+
return text, 0
|
|
344
|
+
clipped = encoded[:max_bytes].decode("utf-8", errors="ignore")
|
|
345
|
+
return clipped, len(encoded) - max_bytes
|