deviatdd 2.5.1__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.
- deviatdd-2.5.1.dist-info/METADATA +386 -0
- deviatdd-2.5.1.dist-info/RECORD +124 -0
- deviatdd-2.5.1.dist-info/WHEEL +4 -0
- deviatdd-2.5.1.dist-info/entry_points.txt +2 -0
- deviatdd-2.5.1.dist-info/licenses/LICENSE +21 -0
- deviate/__init__.py +3 -0
- deviate/cli/__init__.py +824 -0
- deviate/cli/_common.py +155 -0
- deviate/cli/adhoc.py +183 -0
- deviate/cli/constitution.py +113 -0
- deviate/cli/feature.py +94 -0
- deviate/cli/init.py +485 -0
- deviate/cli/inspect.py +208 -0
- deviate/cli/macro.py +937 -0
- deviate/cli/meso.py +1894 -0
- deviate/cli/micro.py +3249 -0
- deviate/cli/review.py +441 -0
- deviate/core/__init__.py +0 -0
- deviate/core/_shared.py +20 -0
- deviate/core/agent.py +505 -0
- deviate/core/cache_discipline.py +65 -0
- deviate/core/commands.py +202 -0
- deviate/core/commit.py +86 -0
- deviate/core/complexity.py +36 -0
- deviate/core/constitution.py +85 -0
- deviate/core/contract.py +17 -0
- deviate/core/convention.py +147 -0
- deviate/core/epic.py +73 -0
- deviate/core/issues.py +46 -0
- deviate/core/prd.py +18 -0
- deviate/core/profile.py +33 -0
- deviate/core/repo.py +47 -0
- deviate/core/run_logger.py +50 -0
- deviate/core/tasks_ledger.py +69 -0
- deviate/core/treesitter/__init__.py +31 -0
- deviate/core/treesitter/analysis.py +457 -0
- deviate/core/treesitter/models.py +35 -0
- deviate/core/treesitter/parser.py +184 -0
- deviate/core/treesitter/queries/bash.scm +7 -0
- deviate/core/treesitter/queries/c_sharp.scm +11 -0
- deviate/core/treesitter/queries/cpp.scm +9 -0
- deviate/core/treesitter/queries/css.scm +4 -0
- deviate/core/treesitter/queries/dockerfile.scm +8 -0
- deviate/core/treesitter/queries/elixir.scm +6 -0
- deviate/core/treesitter/queries/go.scm +9 -0
- deviate/core/treesitter/queries/hcl.scm +3 -0
- deviate/core/treesitter/queries/html.scm +3 -0
- deviate/core/treesitter/queries/javascript.scm +12 -0
- deviate/core/treesitter/queries/json.scm +4 -0
- deviate/core/treesitter/queries/kotlin.scm +8 -0
- deviate/core/treesitter/queries/markdown.scm +4 -0
- deviate/core/treesitter/queries/python.scm +10 -0
- deviate/core/treesitter/queries/rust.scm +12 -0
- deviate/core/treesitter/queries/sql.scm +7 -0
- deviate/core/treesitter/queries/swift.scm +10 -0
- deviate/core/treesitter/queries/toml.scm +3 -0
- deviate/core/treesitter/queries/tsx.scm +14 -0
- deviate/core/treesitter/queries/typescript.scm +14 -0
- deviate/core/treesitter/queries/yaml.scm +4 -0
- deviate/core/validation.py +153 -0
- deviate/core/worktree.py +141 -0
- deviate/main.py +4 -0
- deviate/prompts/__init__.py +0 -0
- deviate/prompts/assembly.py +122 -0
- deviate/prompts/auto/__init__.py +1 -0
- deviate/prompts/auto/execute.md +62 -0
- deviate/prompts/auto/explore.md +103 -0
- deviate/prompts/auto/green.md +137 -0
- deviate/prompts/auto/judge.md +260 -0
- deviate/prompts/auto/plan.md +127 -0
- deviate/prompts/auto/prd.md +108 -0
- deviate/prompts/auto/red.md +156 -0
- deviate/prompts/auto/refactor.md +166 -0
- deviate/prompts/auto/research.md +111 -0
- deviate/prompts/auto/shard.md +90 -0
- deviate/prompts/auto/specify.md +77 -0
- deviate/prompts/auto/tasks.md +191 -0
- deviate/prompts/commands/deviate-adhoc.md +216 -0
- deviate/prompts/commands/deviate-architecture.md +147 -0
- deviate/prompts/commands/deviate-constitution.md +215 -0
- deviate/prompts/commands/deviate-e2e.md +264 -0
- deviate/prompts/commands/deviate-execute.md +223 -0
- deviate/prompts/commands/deviate-explore.md +253 -0
- deviate/prompts/commands/deviate-flows.md +226 -0
- deviate/prompts/commands/deviate-green.md +217 -0
- deviate/prompts/commands/deviate-hotfix.md +188 -0
- deviate/prompts/commands/deviate-init.md +170 -0
- deviate/prompts/commands/deviate-judge.md +193 -0
- deviate/prompts/commands/deviate-merge.md +221 -0
- deviate/prompts/commands/deviate-plan.md +158 -0
- deviate/prompts/commands/deviate-pr.md +144 -0
- deviate/prompts/commands/deviate-prd.md +216 -0
- deviate/prompts/commands/deviate-prune.md +260 -0
- deviate/prompts/commands/deviate-red.md +231 -0
- deviate/prompts/commands/deviate-refactor.md +211 -0
- deviate/prompts/commands/deviate-release.md +123 -0
- deviate/prompts/commands/deviate-research.md +359 -0
- deviate/prompts/commands/deviate-review.md +289 -0
- deviate/prompts/commands/deviate-shard.md +226 -0
- deviate/prompts/commands/deviate-tasks.md +281 -0
- deviate/prompts/commands/deviate-triage.md +141 -0
- deviate/prompts/constitution_seed.md +51 -0
- deviate/prompts/core/core.md +21 -0
- deviate/prompts/core/macro-auto.md +59 -0
- deviate/prompts/core/macro-command.md +54 -0
- deviate/prompts/core/macro-skill.md +54 -0
- deviate/prompts/core/meso-auto.md +63 -0
- deviate/prompts/core/meso-command.md +58 -0
- deviate/prompts/core/meso-skill.md +58 -0
- deviate/prompts/core/micro-auto.md +76 -0
- deviate/prompts/core/micro-command.md +67 -0
- deviate/prompts/core/micro-skill.md +67 -0
- deviate/prompts/extras/deviate-pr-graphite-routing.md +20 -0
- deviate/prompts/governance/__init__.py +0 -0
- deviate/prompts/governance/agents_seed.md +1 -0
- deviate/prompts/governance/claudemd_seed.md +1 -0
- deviate/prompts/governance/graphite_seed.md +18 -0
- deviate/prompts/governance/libref_seed.md +3 -0
- deviate/state/__init__.py +0 -0
- deviate/state/config.py +257 -0
- deviate/state/ledger.py +407 -0
- deviate/ui/__init__.py +5 -0
- deviate/ui/monitor.py +187 -0
- deviate/ui/render.py +26 -0
deviate/cli/review.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from deviate.cli._common import console
|
|
13
|
+
from deviate.core._shared import git_env as _git_env
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_SOURCE_EXTENSIONS: frozenset[str] = frozenset(
|
|
18
|
+
{
|
|
19
|
+
".py",
|
|
20
|
+
".scm",
|
|
21
|
+
".js",
|
|
22
|
+
".mjs",
|
|
23
|
+
".cjs",
|
|
24
|
+
".ts",
|
|
25
|
+
".mts",
|
|
26
|
+
".cts",
|
|
27
|
+
".tsx",
|
|
28
|
+
".rs",
|
|
29
|
+
".go",
|
|
30
|
+
".cpp",
|
|
31
|
+
".cc",
|
|
32
|
+
".cxx",
|
|
33
|
+
".hpp",
|
|
34
|
+
".h",
|
|
35
|
+
".ex",
|
|
36
|
+
".exs",
|
|
37
|
+
".cs",
|
|
38
|
+
".sh",
|
|
39
|
+
".bash",
|
|
40
|
+
".zsh",
|
|
41
|
+
".kt",
|
|
42
|
+
".kts",
|
|
43
|
+
".swift",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_source_file(filepath: str) -> bool:
|
|
49
|
+
"""Check if a filepath corresponds to source code with a meaningful AST."""
|
|
50
|
+
stem, _, ext = filepath.rpartition(".")
|
|
51
|
+
return ("." + ext) in _SOURCE_EXTENSIONS if ext else False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
review_app = typer.Typer(no_args_is_help=True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@review_app.command()
|
|
58
|
+
def pre(
|
|
59
|
+
base: str = typer.Option(
|
|
60
|
+
"main", "--base", help="Base branch for merge-base computation"
|
|
61
|
+
),
|
|
62
|
+
branch: str | None = typer.Option(
|
|
63
|
+
None, "--branch", help="Target branch for self-contained review"
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Gather git state and governance context for review."""
|
|
67
|
+
repo = Path.cwd()
|
|
68
|
+
|
|
69
|
+
target = branch or "HEAD"
|
|
70
|
+
branch_name = branch or _get_current_branch(repo)
|
|
71
|
+
|
|
72
|
+
diff = _compute_diff(repo, base, target)
|
|
73
|
+
entries = _compute_structured_diff(repo, base, target)
|
|
74
|
+
structured_diff_markdown = _format_structured_diff_markdown(entries)
|
|
75
|
+
constitution_path = _resolve_constitution_path(repo)
|
|
76
|
+
prd_path, prd_warning = _resolve_prd(branch_name, repo)
|
|
77
|
+
report_exists = _check_existing_reports(repo)
|
|
78
|
+
|
|
79
|
+
contract = {
|
|
80
|
+
"status": "READY",
|
|
81
|
+
"diff": diff,
|
|
82
|
+
"structured_diff": entries,
|
|
83
|
+
"structured_diff_markdown": structured_diff_markdown,
|
|
84
|
+
"constitution_path": constitution_path,
|
|
85
|
+
"constitution_warning": constitution_path is None,
|
|
86
|
+
"prd_path": prd_path,
|
|
87
|
+
"prd_warning": prd_warning,
|
|
88
|
+
"base_branch": base,
|
|
89
|
+
"report_exists": report_exists,
|
|
90
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
print(json.dumps(contract, indent=2))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_current_branch(repo: Path) -> str | None:
|
|
97
|
+
"""Get current git branch name."""
|
|
98
|
+
try:
|
|
99
|
+
return subprocess.run(
|
|
100
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
101
|
+
cwd=repo,
|
|
102
|
+
env=_git_env(),
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
check=True,
|
|
106
|
+
).stdout.strip()
|
|
107
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _compute_merge_base(commit_a: str, commit_b: str, repo: Path) -> str:
|
|
112
|
+
"""Compute merge base between two commits."""
|
|
113
|
+
try:
|
|
114
|
+
return subprocess.run(
|
|
115
|
+
["git", "merge-base", commit_a, commit_b],
|
|
116
|
+
cwd=repo,
|
|
117
|
+
env=_git_env(),
|
|
118
|
+
capture_output=True,
|
|
119
|
+
text=True,
|
|
120
|
+
check=True,
|
|
121
|
+
).stdout.strip()
|
|
122
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
123
|
+
return ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _gather_diff(base: str, head: str, repo: Path) -> str:
|
|
127
|
+
"""Gather unified diff between base and head commits."""
|
|
128
|
+
try:
|
|
129
|
+
return subprocess.run(
|
|
130
|
+
["git", "diff", f"{base}..{head}"],
|
|
131
|
+
cwd=repo,
|
|
132
|
+
env=_git_env(),
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
check=True,
|
|
136
|
+
).stdout
|
|
137
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _compute_diff(repo: Path, base: str = "main", target_branch: str = "HEAD") -> str:
|
|
142
|
+
"""Compute unified diff against merge-base with given base branch."""
|
|
143
|
+
merge_base = _compute_merge_base(base, target_branch, repo)
|
|
144
|
+
if not merge_base:
|
|
145
|
+
return ""
|
|
146
|
+
return _gather_diff(merge_base, target_branch, repo)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _resolve_constitution_path(repo: Path) -> str | None:
|
|
150
|
+
"""Resolve specs/constitution.md path if it exists."""
|
|
151
|
+
path = repo / "specs" / "constitution.md"
|
|
152
|
+
if path.exists():
|
|
153
|
+
return str(path.resolve())
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_prd(branch_name: str | None, repo: Path) -> tuple[str | None, bool]:
|
|
158
|
+
"""Resolve PRD path with epic priority over adhoc fallback."""
|
|
159
|
+
epic_slug = None
|
|
160
|
+
if branch_name:
|
|
161
|
+
parts = branch_name.split("/")
|
|
162
|
+
if len(parts) > 1:
|
|
163
|
+
epic_slug = parts[1]
|
|
164
|
+
|
|
165
|
+
if epic_slug:
|
|
166
|
+
epic_prd = repo / "specs" / epic_slug / "prd.md"
|
|
167
|
+
if epic_prd.exists():
|
|
168
|
+
return str(epic_prd.resolve()), False
|
|
169
|
+
|
|
170
|
+
adhoc_prd = repo / "specs" / "adhoc" / "prd.md"
|
|
171
|
+
if adhoc_prd.exists():
|
|
172
|
+
return str(adhoc_prd.resolve()), False
|
|
173
|
+
|
|
174
|
+
return None, True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _reports_dir(repo: Path) -> Path:
|
|
178
|
+
"""Resolve the .deviate/review/reports/ directory path."""
|
|
179
|
+
return repo / ".deviate" / "review" / "reports"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _check_existing_reports(repo: Path) -> bool:
|
|
183
|
+
"""Check if review reports already exist under .deviate/review/reports/."""
|
|
184
|
+
reports_dir = _reports_dir(repo)
|
|
185
|
+
if not reports_dir.is_dir():
|
|
186
|
+
return False
|
|
187
|
+
return any(reports_dir.iterdir())
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _parse_diff_filepaths(diff_text: str) -> list[str]:
|
|
191
|
+
paths: list[str] = []
|
|
192
|
+
for line in diff_text.splitlines():
|
|
193
|
+
if line.startswith("diff --git"):
|
|
194
|
+
parts = line.split()
|
|
195
|
+
if len(parts) >= 4:
|
|
196
|
+
b_path = parts[-1].lstrip("b/")
|
|
197
|
+
paths.append(b_path)
|
|
198
|
+
return paths
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _compute_file_stats(diff_text: str, target_filepath: str) -> dict:
|
|
202
|
+
"""Compute file-level stats: net_lines_changed, chunks_changed, chunks."""
|
|
203
|
+
added = 0
|
|
204
|
+
removed = 0
|
|
205
|
+
chunks = 0
|
|
206
|
+
in_target = False
|
|
207
|
+
for line in diff_text.splitlines():
|
|
208
|
+
if line.startswith("diff --git"):
|
|
209
|
+
parts = line.split()
|
|
210
|
+
b_path = parts[-1].lstrip("b/") if len(parts) >= 4 else ""
|
|
211
|
+
in_target = b_path == target_filepath
|
|
212
|
+
continue
|
|
213
|
+
if not in_target:
|
|
214
|
+
continue
|
|
215
|
+
if line.startswith("@@"):
|
|
216
|
+
chunks += 1
|
|
217
|
+
continue
|
|
218
|
+
if (
|
|
219
|
+
line.startswith("--- ")
|
|
220
|
+
or line.startswith("+++ ")
|
|
221
|
+
or line.startswith("index ")
|
|
222
|
+
):
|
|
223
|
+
continue
|
|
224
|
+
if line.startswith("new file"):
|
|
225
|
+
continue
|
|
226
|
+
if line.startswith("deleted file"):
|
|
227
|
+
continue
|
|
228
|
+
if line.startswith("+"):
|
|
229
|
+
added += 1
|
|
230
|
+
elif line.startswith("-"):
|
|
231
|
+
removed += 1
|
|
232
|
+
net_str = f"+{added}/-{removed}"
|
|
233
|
+
return {
|
|
234
|
+
"net_lines_changed": net_str,
|
|
235
|
+
"lines_added": added,
|
|
236
|
+
"lines_removed": removed,
|
|
237
|
+
"chunks_changed": chunks,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_diff_imports(
|
|
242
|
+
diff_text: str, target_filepath: str, extract_imports_fn
|
|
243
|
+
) -> dict:
|
|
244
|
+
"""Parse added/removed import lines from diff for target filepath."""
|
|
245
|
+
added: list[str] = []
|
|
246
|
+
removed: list[str] = []
|
|
247
|
+
in_target = False
|
|
248
|
+
for line in diff_text.splitlines():
|
|
249
|
+
if line.startswith("diff --git"):
|
|
250
|
+
parts = line.split()
|
|
251
|
+
b_path = parts[-1].lstrip("b/") if len(parts) >= 4 else ""
|
|
252
|
+
in_target = b_path == target_filepath
|
|
253
|
+
continue
|
|
254
|
+
if not in_target:
|
|
255
|
+
continue
|
|
256
|
+
if (
|
|
257
|
+
line.startswith("--- ")
|
|
258
|
+
or line.startswith("+++ ")
|
|
259
|
+
or line.startswith("index ")
|
|
260
|
+
):
|
|
261
|
+
continue
|
|
262
|
+
if line.startswith("@@"):
|
|
263
|
+
continue
|
|
264
|
+
content = line[1:] if len(line) > 1 else ""
|
|
265
|
+
stripped = content.strip()
|
|
266
|
+
if not stripped:
|
|
267
|
+
continue
|
|
268
|
+
if line.startswith("+"):
|
|
269
|
+
if extract_imports_fn(stripped):
|
|
270
|
+
added.append(stripped)
|
|
271
|
+
elif line.startswith("-"):
|
|
272
|
+
if extract_imports_fn(stripped):
|
|
273
|
+
removed.append(stripped)
|
|
274
|
+
return {"imports_added": added, "imports_removed": removed}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _is_import_line(line: str) -> bool:
|
|
278
|
+
"""Heuristic check if a line looks like an import/include/using directive."""
|
|
279
|
+
lower = line.lower().strip()
|
|
280
|
+
if lower.startswith(
|
|
281
|
+
(
|
|
282
|
+
"import ",
|
|
283
|
+
"from ",
|
|
284
|
+
"using ",
|
|
285
|
+
"include ",
|
|
286
|
+
"#include",
|
|
287
|
+
"use ",
|
|
288
|
+
"extern crate",
|
|
289
|
+
"require(",
|
|
290
|
+
)
|
|
291
|
+
):
|
|
292
|
+
return True
|
|
293
|
+
if lower.startswith("const ") and ("require(" in lower or "import(" in lower):
|
|
294
|
+
return True
|
|
295
|
+
if lower.startswith("#") and "include" in lower:
|
|
296
|
+
return True
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _build_file_entry(
|
|
301
|
+
filepath: str, language: str, symbols_raw, diff_text: str
|
|
302
|
+
) -> dict:
|
|
303
|
+
"""Build a file entry dict for the structured diff contract."""
|
|
304
|
+
stats = _compute_file_stats(diff_text, filepath)
|
|
305
|
+
entry: dict = {
|
|
306
|
+
"file": filepath,
|
|
307
|
+
"language": language,
|
|
308
|
+
"net_lines_changed": stats["net_lines_changed"],
|
|
309
|
+
"lines_added": stats["lines_added"],
|
|
310
|
+
"lines_removed": stats["lines_removed"],
|
|
311
|
+
"chunks_changed": stats["chunks_changed"],
|
|
312
|
+
}
|
|
313
|
+
symbols_list: list[dict] = []
|
|
314
|
+
for sc in symbols_raw:
|
|
315
|
+
sym: dict[str, str | int] = {
|
|
316
|
+
"k": sc.kind,
|
|
317
|
+
"n": sc.name,
|
|
318
|
+
"c": sc.change,
|
|
319
|
+
}
|
|
320
|
+
if sc.start_line or sc.end_line:
|
|
321
|
+
sym["L"] = f"{sc.start_line}-{sc.end_line}"
|
|
322
|
+
if sc.old_start_line or sc.old_end_line:
|
|
323
|
+
sym["LO"] = f"{sc.old_start_line}-{sc.old_end_line}"
|
|
324
|
+
if sc.old_signature:
|
|
325
|
+
sym["SO"] = sc.old_signature[:80]
|
|
326
|
+
if sc.new_signature:
|
|
327
|
+
sym["SN"] = sc.new_signature[:80]
|
|
328
|
+
if sc.old_line_count or sc.new_line_count:
|
|
329
|
+
sym["S"] = f"{sc.old_line_count}→{sc.new_line_count}"
|
|
330
|
+
symbols_list.append(sym)
|
|
331
|
+
entry["symbols"] = symbols_list
|
|
332
|
+
imports = _parse_diff_imports(diff_text, filepath, _is_import_line)
|
|
333
|
+
if imports["imports_added"] or imports["imports_removed"]:
|
|
334
|
+
entry["+I"] = imports["imports_added"]
|
|
335
|
+
entry["-I"] = imports["imports_removed"]
|
|
336
|
+
return entry
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _format_structured_diff_markdown(entries: list[dict]) -> str:
|
|
340
|
+
"""Render structured diff as a compact markdown table — token-efficient for LLM."""
|
|
341
|
+
sections: list[str] = []
|
|
342
|
+
for ent in entries:
|
|
343
|
+
fp = ent["file"]
|
|
344
|
+
lang = ent["language"]
|
|
345
|
+
net = ent["net_lines_changed"]
|
|
346
|
+
lines = [f"### {fp} ({lang}, {net})"]
|
|
347
|
+
syms = ent.get("symbols", [])
|
|
348
|
+
if syms:
|
|
349
|
+
lines.append("| k | n | c | L | LO | S | SO | SN |")
|
|
350
|
+
lines.append("|---|---|---|---|---|---|---|---|")
|
|
351
|
+
for s in syms:
|
|
352
|
+
lines.append(
|
|
353
|
+
f"| {s.get('k', '-')} "
|
|
354
|
+
f"| {s.get('n', '-')} "
|
|
355
|
+
f"| {s.get('c', '-')} "
|
|
356
|
+
f"| {s.get('L', '-')} "
|
|
357
|
+
f"| {s.get('LO', '-')} "
|
|
358
|
+
f"| {s.get('S', '-')} "
|
|
359
|
+
f"| {s.get('SO', '-')[:40]} "
|
|
360
|
+
f"| {s.get('SN', '-')[:40]} |"
|
|
361
|
+
)
|
|
362
|
+
added = ent.get("+I", [])
|
|
363
|
+
removed = ent.get("-I", [])
|
|
364
|
+
if added or removed:
|
|
365
|
+
lines.append("")
|
|
366
|
+
if added:
|
|
367
|
+
for imp in added:
|
|
368
|
+
lines.append(f"+ `{imp}`")
|
|
369
|
+
if removed:
|
|
370
|
+
for imp in removed:
|
|
371
|
+
lines.append(f"- `{imp}`")
|
|
372
|
+
sections.append("\n".join(lines))
|
|
373
|
+
return "\n\n".join(sections) if sections else "(no source changes)"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _compute_structured_diff(repo: Path, base: str, target: str) -> list[dict]:
|
|
377
|
+
"""Compute structured AST diff entries for ALL changed files.
|
|
378
|
+
|
|
379
|
+
Source files get full symbol-level AST diff breakdown.
|
|
380
|
+
Non-source files get empty symbols and ``unknown`` language.
|
|
381
|
+
Returns empty list when no diff or tree-sitter unavailable.
|
|
382
|
+
"""
|
|
383
|
+
merge_base = _compute_merge_base(base, target, repo)
|
|
384
|
+
if not merge_base:
|
|
385
|
+
return []
|
|
386
|
+
|
|
387
|
+
diff_text = _gather_diff(merge_base, target, repo)
|
|
388
|
+
if not diff_text.strip():
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
from deviate.core.treesitter import extract_changed_symbols, get_language_id
|
|
393
|
+
except ImportError:
|
|
394
|
+
logger.warning("tree-sitter not available — skipping structured diff")
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
filepaths = _parse_diff_filepaths(diff_text)
|
|
398
|
+
entries: list[dict] = []
|
|
399
|
+
|
|
400
|
+
for filepath in filepaths:
|
|
401
|
+
try:
|
|
402
|
+
if _is_source_file(filepath):
|
|
403
|
+
symbols = extract_changed_symbols(diff_text, filepath)
|
|
404
|
+
language = (
|
|
405
|
+
symbols[0].language
|
|
406
|
+
if symbols
|
|
407
|
+
else get_language_id(filepath) or "unknown"
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
symbols = []
|
|
411
|
+
language = "unknown"
|
|
412
|
+
entries.append(_build_file_entry(filepath, language, symbols, diff_text))
|
|
413
|
+
except (LookupError, TypeError, ValueError):
|
|
414
|
+
logger.warning("Failed to compute structured diff for: %s", filepath)
|
|
415
|
+
|
|
416
|
+
return entries
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@review_app.command()
|
|
420
|
+
def post(
|
|
421
|
+
content: str | None = typer.Argument(
|
|
422
|
+
None, help="Report markdown content. If not provided, reads from stdin."
|
|
423
|
+
),
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Persist review report and mark review complete."""
|
|
426
|
+
if not content:
|
|
427
|
+
if not sys.stdin.isatty():
|
|
428
|
+
content = sys.stdin.read()
|
|
429
|
+
|
|
430
|
+
if not content:
|
|
431
|
+
console.print("[yellow]SKIP[/] no report content provided")
|
|
432
|
+
raise typer.Exit(code=0)
|
|
433
|
+
|
|
434
|
+
repo = Path.cwd()
|
|
435
|
+
reports_dir = _reports_dir(repo)
|
|
436
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
437
|
+
|
|
438
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
|
|
439
|
+
report_file = reports_dir / f"review-report-{timestamp}.md"
|
|
440
|
+
report_file.write_text(content, encoding="utf-8")
|
|
441
|
+
console.print(f"[green]OK[/] report written to {report_file}")
|
deviate/core/__init__.py
ADDED
|
File without changes
|
deviate/core/_shared.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def git_env() -> dict[str, str]:
|
|
7
|
+
"""Return os.environ with GIT_* and GH_* stripped.
|
|
8
|
+
|
|
9
|
+
Production code MUST pass this as the `env` of every `git`/`gh`/`gt`
|
|
10
|
+
subprocess call so child processes don't inherit the parent's git
|
|
11
|
+
identity, remotes, or auth state. Prefer creating branch refs
|
|
12
|
+
(`git branch <name>`) over `git checkout -b` in non-interactive code;
|
|
13
|
+
if a checkout is unavoidable, save `git rev-parse --abbrev-ref HEAD`
|
|
14
|
+
first and restore it afterwards.
|
|
15
|
+
"""
|
|
16
|
+
return {
|
|
17
|
+
k: v
|
|
18
|
+
for k, v in os.environ.items()
|
|
19
|
+
if not k.startswith("GIT_") and not k.startswith("GH_")
|
|
20
|
+
}
|