semantic-code-review 0.24.2__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 (100) hide show
  1. semantic_code_review/__init__.py +5 -0
  2. semantic_code_review/augment/__init__.py +3 -0
  3. semantic_code_review/augment/agents.py +95 -0
  4. semantic_code_review/augment/console.py +440 -0
  5. semantic_code_review/augment/extra_review.py +237 -0
  6. semantic_code_review/augment/fold_summary.py +447 -0
  7. semantic_code_review/augment/hunks.py +264 -0
  8. semantic_code_review/augment/mcp_server.py +156 -0
  9. semantic_code_review/augment/overview.py +233 -0
  10. semantic_code_review/augment/pass_.py +171 -0
  11. semantic_code_review/augment/pipeline.py +571 -0
  12. semantic_code_review/augment/progress.py +223 -0
  13. semantic_code_review/augment/prompts.py +93 -0
  14. semantic_code_review/augment/schemas.py +408 -0
  15. semantic_code_review/augment/tools.py +475 -0
  16. semantic_code_review/augment/trace_adapter.py +369 -0
  17. semantic_code_review/backends/__init__.py +95 -0
  18. semantic_code_review/backends/_cli_driver.py +571 -0
  19. semantic_code_review/backends/anthropic_sdk.py +56 -0
  20. semantic_code_review/backends/base.py +104 -0
  21. semantic_code_review/backends/claude_cli.py +421 -0
  22. semantic_code_review/backends/google_sdk.py +73 -0
  23. semantic_code_review/backends/openai_compat.py +42 -0
  24. semantic_code_review/cache/__init__.py +3 -0
  25. semantic_code_review/cache/store.py +87 -0
  26. semantic_code_review/cli/__init__.py +70 -0
  27. semantic_code_review/cli/_shared.py +157 -0
  28. semantic_code_review/cli/augment.py +74 -0
  29. semantic_code_review/cli/config_cmd.py +266 -0
  30. semantic_code_review/cli/fetch.py +33 -0
  31. semantic_code_review/cli/lint.py +27 -0
  32. semantic_code_review/cli/pr.py +98 -0
  33. semantic_code_review/cli/review.py +104 -0
  34. semantic_code_review/cli/runs_cmd.py +17 -0
  35. semantic_code_review/cli/show.py +19 -0
  36. semantic_code_review/cli/strip.py +20 -0
  37. semantic_code_review/config.py +563 -0
  38. semantic_code_review/config_template.py +154 -0
  39. semantic_code_review/fetch/__init__.py +61 -0
  40. semantic_code_review/fetch/anchor.py +244 -0
  41. semantic_code_review/fetch/github.py +229 -0
  42. semantic_code_review/fetch/github_comments.py +404 -0
  43. semantic_code_review/fetch/local.py +443 -0
  44. semantic_code_review/fetch/run_source.py +70 -0
  45. semantic_code_review/format/__init__.py +3 -0
  46. semantic_code_review/format/emit.py +163 -0
  47. semantic_code_review/format/lint.py +74 -0
  48. semantic_code_review/format/parse.py +574 -0
  49. semantic_code_review/format/sidecar.py +19 -0
  50. semantic_code_review/format/strip.py +20 -0
  51. semantic_code_review/git_ops.py +252 -0
  52. semantic_code_review/paths.py +61 -0
  53. semantic_code_review/review/__init__.py +3 -0
  54. semantic_code_review/review/comments.py +176 -0
  55. semantic_code_review/review/github.py +284 -0
  56. semantic_code_review/review/github_graphql.py +433 -0
  57. semantic_code_review/review/pr_flow.py +299 -0
  58. semantic_code_review/review/runner.py +407 -0
  59. semantic_code_review/review/server.py +1043 -0
  60. semantic_code_review/structural/__init__.py +41 -0
  61. semantic_code_review/structural/diff.py +102 -0
  62. semantic_code_review/structural/parse.py +330 -0
  63. semantic_code_review/structural/symbols.py +43 -0
  64. semantic_code_review/viewer/__init__.py +3 -0
  65. semantic_code_review/viewer/assets/annotations.ts +547 -0
  66. semantic_code_review/viewer/assets/boot.ts +273 -0
  67. semantic_code_review/viewer/assets/comment_store.ts +121 -0
  68. semantic_code_review/viewer/assets/comments.ts +624 -0
  69. semantic_code_review/viewer/assets/console.ts +426 -0
  70. semantic_code_review/viewer/assets/console_render.ts +213 -0
  71. semantic_code_review/viewer/assets/console_selection.ts +102 -0
  72. semantic_code_review/viewer/assets/data_store.ts +156 -0
  73. semantic_code_review/viewer/assets/file_rows.ts +49 -0
  74. semantic_code_review/viewer/assets/folds.ts +560 -0
  75. semantic_code_review/viewer/assets/index.html +62 -0
  76. semantic_code_review/viewer/assets/post_modal.ts +383 -0
  77. semantic_code_review/viewer/assets/progress.ts +138 -0
  78. semantic_code_review/viewer/assets/render.ts +937 -0
  79. semantic_code_review/viewer/assets/sidebar.ts +429 -0
  80. semantic_code_review/viewer/assets/sse.ts +78 -0
  81. semantic_code_review/viewer/assets/text_highlight.ts +215 -0
  82. semantic_code_review/viewer/assets/types.d.ts +388 -0
  83. semantic_code_review/viewer/assets/vendor/LICENSE +29 -0
  84. semantic_code_review/viewer/assets/vendor/VENDOR.md +55 -0
  85. semantic_code_review/viewer/assets/vendor/github-dark.min.css +10 -0
  86. semantic_code_review/viewer/assets/vendor/github.min.css +10 -0
  87. semantic_code_review/viewer/assets/vendor/highlight.min.js +1244 -0
  88. semantic_code_review/viewer/assets/vendor/mermaid.LICENSE +21 -0
  89. semantic_code_review/viewer/assets/vendor/mermaid.min.js +3587 -0
  90. semantic_code_review/viewer/assets/vendor/refresh.sh +65 -0
  91. semantic_code_review/viewer/assets/viewer.css +1202 -0
  92. semantic_code_review/viewer/assets/viewer.js +10588 -0
  93. semantic_code_review/viewer/build_json.py +546 -0
  94. semantic_code_review/viewer/hunk_layout.py +471 -0
  95. semantic_code_review-0.24.2.dist-info/METADATA +348 -0
  96. semantic_code_review-0.24.2.dist-info/RECORD +100 -0
  97. semantic_code_review-0.24.2.dist-info/WHEEL +5 -0
  98. semantic_code_review-0.24.2.dist-info/entry_points.txt +2 -0
  99. semantic_code_review-0.24.2.dist-info/licenses/LICENSE +21 -0
  100. semantic_code_review-0.24.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,369 @@
1
+ """Translate a pydantic-ai `AgentRunResult` to the per-iteration trace
2
+ JSON shape `run_agentic` (now retired) used to write.
3
+
4
+ Every consumer of `trace/*.json` reads this shape (the viewer's
5
+ trace tab, ad-hoc tooling, support diagnostics). Keeping it stable
6
+ across the SDK / CLI migration means downstream code doesn't notice
7
+ which loop driver produced the run.
8
+
9
+ The shape:
10
+
11
+ {
12
+ "model": str,
13
+ "system": str,
14
+ "tools": [tool_name, ...],
15
+ "submit_tool": str, # name of the structured-output sink
16
+ "iterations": [
17
+ {
18
+ "messages_sent": [{"role", "content"}, ...],
19
+ "response": {"model", "role", "stop_reason", "usage", "content"},
20
+ "tool_results": [{"type": "tool_result", "tool_use_id", "content"}, ...]
21
+ },
22
+ ...
23
+ ],
24
+ "result": {
25
+ "submit_args": dict, # the validated output_type instance, dumped
26
+ "tool_calls": [{"name", "input", "result_len"}, ...],
27
+ "input_tokens": int,
28
+ "output_tokens": int,
29
+ "cache_read_tokens": int,
30
+ },
31
+ "error": { # present iff the run failed before submit
32
+ "type": str, # exception class name
33
+ "message": str, # str(exc)
34
+ },
35
+ }
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import json
41
+ from pathlib import Path
42
+ from typing import Any
43
+
44
+ from pydantic_ai import CachePoint
45
+ from pydantic_ai.messages import (
46
+ ModelRequest,
47
+ ModelResponse,
48
+ RetryPromptPart,
49
+ SystemPromptPart,
50
+ TextPart,
51
+ ToolCallPart,
52
+ ToolReturnPart,
53
+ UserPromptPart,
54
+ )
55
+
56
+ #: Cap for the fallback `repr` on unrecognised parts. Long thinking
57
+ #: blocks can be tens of KB; trimming keeps traces inspectable.
58
+ _FALLBACK_REPR_CAP = 5000
59
+
60
+
61
+ def _trace_safe_user_content(content: Any) -> Any:
62
+ """Render a UserPromptPart.content value into a JSON-safe shape.
63
+
64
+ A list-form user prompt can contain `CachePoint` markers (used for
65
+ Anthropic prompt caching) that aren't JSON-serialisable. We render
66
+ them as a sentinel dict so a human reading the trace can still see
67
+ where the cache breakpoints fell, without breaking `json.dumps`.
68
+ """
69
+ if isinstance(content, str):
70
+ return content
71
+ if isinstance(content, list):
72
+ out: list[Any] = []
73
+ for item in content:
74
+ if isinstance(item, CachePoint):
75
+ out.append({"type": "cache-point", "ttl": item.ttl})
76
+ else:
77
+ out.append(item)
78
+ return out
79
+ return content
80
+
81
+
82
+ def _request_to_sent(req: ModelRequest) -> list[dict[str, Any]]:
83
+ """Translate a ModelRequest's parts into the legacy `messages_sent` shape.
84
+
85
+ System prompts are intentionally dropped — the trace records the
86
+ system prompt once at the top level. Anything else we don't have
87
+ explicit handling for (FilePart, InstructionPart, …) falls through
88
+ to a generic dump so the trace doesn't silently lose information.
89
+ """
90
+ out: list[dict[str, Any]] = []
91
+ for part in req.parts:
92
+ if isinstance(part, SystemPromptPart):
93
+ continue
94
+ if isinstance(part, UserPromptPart):
95
+ out.append({"role": "user", "content": _trace_safe_user_content(part.content)})
96
+ elif isinstance(part, ToolReturnPart):
97
+ out.append(
98
+ {
99
+ "role": "user",
100
+ "content": [
101
+ {
102
+ "type": "tool_result",
103
+ "tool_use_id": part.tool_call_id,
104
+ "content": _stringify(part.content),
105
+ }
106
+ ],
107
+ }
108
+ )
109
+ elif isinstance(part, RetryPromptPart):
110
+ out.append(
111
+ {
112
+ "role": "user",
113
+ "content": [
114
+ {
115
+ "type": "tool_result",
116
+ "tool_use_id": part.tool_call_id or "",
117
+ "content": _stringify(part.content),
118
+ "is_error": True,
119
+ }
120
+ ],
121
+ }
122
+ )
123
+ else:
124
+ out.append({"role": "unknown", "content": _fallback_part(part)})
125
+ return out
126
+
127
+
128
+ def _response_content(resp: ModelResponse) -> list[dict[str, Any]]:
129
+ """Flatten ModelResponse parts into the legacy assistant `content` blocks.
130
+
131
+ Parts the legacy shape doesn't have a slot for (ThinkingPart,
132
+ BuiltinToolCallPart, …) fall through to a generic dump so a
133
+ misbehaving model run's trace still shows what the model emitted.
134
+ """
135
+ out: list[dict[str, Any]] = []
136
+ for part in resp.parts:
137
+ if isinstance(part, TextPart):
138
+ out.append({"type": "text", "text": part.content})
139
+ elif isinstance(part, ToolCallPart):
140
+ try:
141
+ input_ = part.args_as_dict()
142
+ except Exception as e: # noqa: BLE001 — args may be unparseable JSON
143
+ # The malformed args themselves are exactly what we
144
+ # want to see when a tool-output validation fails.
145
+ input_ = {
146
+ "_raw": getattr(part, "args", None),
147
+ "_parse_error": f"{type(e).__name__}: {e}",
148
+ }
149
+ out.append(
150
+ {
151
+ "type": "tool_use",
152
+ "id": part.tool_call_id or "",
153
+ "name": part.tool_name,
154
+ "input": input_,
155
+ }
156
+ )
157
+ else:
158
+ out.append(_fallback_part(part))
159
+ return out
160
+
161
+
162
+ def _fallback_part(part: Any) -> dict[str, Any]:
163
+ """Generic dump for message parts we don't render specifically.
164
+
165
+ Carries the class name and a truncated `repr` so trace readers can
166
+ see at minimum what kind of part the model emitted, and (for short
167
+ parts) its content. Common targets: ThinkingPart from extended-
168
+ thinking models, BuiltinToolCallPart / BuiltinToolReturnPart from
169
+ server-side tool surfaces, FilePart / InstructionPart from
170
+ multimodal prompts.
171
+ """
172
+ text = repr(part)
173
+ if len(text) > _FALLBACK_REPR_CAP:
174
+ text = text[:_FALLBACK_REPR_CAP] + "…(truncated)"
175
+ return {"type": type(part).__name__, "repr": text}
176
+
177
+
178
+ def _response_to_dict(resp: ModelResponse) -> dict[str, Any]:
179
+ usage = resp.usage
180
+ return {
181
+ "id": resp.provider_response_id or "",
182
+ "model": resp.model_name or "",
183
+ "role": "assistant",
184
+ "stop_reason": resp.finish_reason or "",
185
+ "usage": {
186
+ "input_tokens": (usage.input_tokens or 0) if usage else 0,
187
+ "output_tokens": (usage.output_tokens or 0) if usage else 0,
188
+ "cache_creation_input_tokens": (usage.cache_write_tokens or 0) if usage else 0,
189
+ "cache_read_input_tokens": (usage.cache_read_tokens or 0) if usage else 0,
190
+ },
191
+ "content": _response_content(resp),
192
+ }
193
+
194
+
195
+ def _tool_returns_in(req: ModelRequest) -> list[dict[str, Any]]:
196
+ """Tool results that follow a response (carried by the next request)."""
197
+ out: list[dict[str, Any]] = []
198
+ for part in req.parts:
199
+ if isinstance(part, ToolReturnPart):
200
+ out.append(
201
+ {
202
+ "type": "tool_result",
203
+ "tool_use_id": part.tool_call_id,
204
+ "content": _stringify(part.content),
205
+ }
206
+ )
207
+ elif isinstance(part, RetryPromptPart):
208
+ out.append(
209
+ {
210
+ "type": "tool_result",
211
+ "tool_use_id": part.tool_call_id or "",
212
+ "content": _stringify(part.content),
213
+ "is_error": True,
214
+ }
215
+ )
216
+ return out
217
+
218
+
219
+ def _stringify(content: Any) -> str:
220
+ if isinstance(content, str):
221
+ return content
222
+ try:
223
+ return json.dumps(content, ensure_ascii=False)
224
+ except TypeError:
225
+ return str(content)
226
+
227
+
228
+ def write_pydantic_ai_trace(
229
+ result: Any,
230
+ *,
231
+ trace_path: Path,
232
+ model: str,
233
+ system: str,
234
+ tool_names: list[str],
235
+ submit_tool: str,
236
+ ) -> None:
237
+ """Render an `AgentRunResult` into the legacy trace shape and write it."""
238
+ submit_args = _submit_args_from_output(getattr(result, "output", None))
239
+ write_partial_trace(
240
+ list(result.all_messages()),
241
+ trace_path=trace_path,
242
+ model=model,
243
+ system=system,
244
+ tool_names=tool_names,
245
+ submit_tool=submit_tool,
246
+ submit_args=submit_args,
247
+ )
248
+
249
+
250
+ def write_partial_trace(
251
+ messages: list[Any],
252
+ *,
253
+ trace_path: Path,
254
+ model: str,
255
+ system: str,
256
+ tool_names: list[str],
257
+ submit_tool: str,
258
+ submit_args: dict[str, Any] | None = None,
259
+ error: BaseException | None = None,
260
+ ) -> None:
261
+ """Write a trace for a (possibly partial) message history.
262
+
263
+ Distinct entry point from ``write_pydantic_ai_trace`` because the
264
+ caller drives ``agent.iter()`` rather than ``agent.run()`` — on a
265
+ `UsageLimitExceeded` or similar mid-run failure there is no
266
+ `AgentRunResult` to extract `output`/`all_messages()` from, but
267
+ the partial message history is still on the `AgentRun`.
268
+ """
269
+ requests = [m for m in messages if isinstance(m, ModelRequest)]
270
+ responses = [m for m in messages if isinstance(m, ModelResponse)]
271
+
272
+ iterations: list[dict[str, Any]] = []
273
+ cumulative: list[dict[str, Any]] = []
274
+ for idx, resp in enumerate(responses):
275
+ if idx < len(requests):
276
+ cumulative.extend(_request_to_sent(requests[idx]))
277
+ sent_snapshot = [dict(m) for m in cumulative]
278
+ response_dict = _response_to_dict(resp)
279
+ cumulative.append({"role": "assistant", "content": response_dict["content"]})
280
+ if idx + 1 < len(requests):
281
+ tool_results = _tool_returns_in(requests[idx + 1])
282
+ else:
283
+ tool_results = []
284
+ iterations.append(
285
+ {
286
+ "messages_sent": sent_snapshot,
287
+ "response": response_dict,
288
+ "tool_results": tool_results,
289
+ }
290
+ )
291
+
292
+ # Trailing request with no matching response — happens on the
293
+ # failure path when the agent loop raised before the model
294
+ # replied (e.g. usage-limit fired on the first call, or the
295
+ # provider returned an error). Surface the prompt anyway so the
296
+ # diagnostic trace shows what was sent.
297
+ if len(requests) > len(responses):
298
+ cumulative.extend(_request_to_sent(requests[len(responses)]))
299
+ iterations.append(
300
+ {
301
+ "messages_sent": [dict(m) for m in cumulative],
302
+ "response": None,
303
+ "tool_results": [],
304
+ }
305
+ )
306
+
307
+ input_t = output_t = cache_t = 0
308
+ for resp in responses:
309
+ u = resp.usage
310
+ if u is None:
311
+ continue
312
+ input_t += u.input_tokens or 0
313
+ output_t += u.output_tokens or 0
314
+ cache_t += u.cache_read_tokens or 0
315
+
316
+ if submit_args is None:
317
+ submit_args = {}
318
+
319
+ tool_calls: list[dict[str, Any]] = []
320
+ # Map tool_call_id -> result content length (taken from subsequent
321
+ # ToolReturnParts) so the legacy `result_len` field stays populated.
322
+ result_lens: dict[str, int] = {}
323
+ for req in requests:
324
+ for part in req.parts:
325
+ if isinstance(part, ToolReturnPart):
326
+ result_lens[part.tool_call_id] = len(_stringify(part.content))
327
+ for resp in responses:
328
+ for part in resp.parts:
329
+ if isinstance(part, ToolCallPart):
330
+ tool_calls.append(
331
+ {
332
+ "name": part.tool_name,
333
+ "input": part.args_as_dict(),
334
+ "result_len": result_lens.get(part.tool_call_id or "", 0),
335
+ }
336
+ )
337
+
338
+ trace: dict[str, Any] = {
339
+ "model": model,
340
+ "system": system,
341
+ "tools": tool_names,
342
+ "submit_tool": submit_tool,
343
+ "iterations": iterations,
344
+ "result": {
345
+ "submit_args": submit_args,
346
+ "tool_calls": tool_calls,
347
+ "input_tokens": input_t,
348
+ "output_tokens": output_t,
349
+ "cache_read_tokens": cache_t,
350
+ },
351
+ }
352
+ if error is not None:
353
+ trace["error"] = {"type": type(error).__name__, "message": str(error)}
354
+ trace_path.parent.mkdir(parents=True, exist_ok=True)
355
+ trace_path.write_text(json.dumps(trace, indent=2, ensure_ascii=False), encoding="utf-8")
356
+
357
+
358
+ def _submit_args_from_output(output: Any) -> dict[str, Any]:
359
+ if output is None:
360
+ return {}
361
+ if hasattr(output, "model_dump"):
362
+ return output.model_dump(by_alias=True)
363
+ return {}
364
+
365
+
366
+ def submit_args_from_result(result: Any) -> dict[str, Any]:
367
+ """Extract the validated output as a dict that `apply_*_to_diff` can consume."""
368
+ output = result.output
369
+ return output.model_dump(by_alias=True)
@@ -0,0 +1,95 @@
1
+ """Backend registry — name → adapter dispatch.
2
+
3
+ The CLI calls `get(name, config=...)` to obtain a `Backend` instance,
4
+ then `.resolve(model=...)` for the `Client` that drives the augment
5
+ pipeline. `resolve_auto(config=...)` picks a backend when the user
6
+ did not pass `--backend`.
7
+
8
+ The registry maps `BackendType` to a `Backend` subclass. Builtins
9
+ (`claude-api`, `claude-cli`, `gemini-api`, plus the
10
+ openai-compat presets) auto-register at import time; user-defined
11
+ `[backends.<name>]` entries reuse the same dispatch by virtue of
12
+ sharing a `BackendType`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import typer
18
+
19
+ from ..config import BackendType, ScrConfig
20
+ from .anthropic_sdk import AnthropicSdkBackend
21
+ from .base import Backend
22
+ from .claude_cli import ClaudeCliBackend
23
+ from .google_sdk import GoogleSdkBackend
24
+ from .openai_compat import OpenAICompatBackend
25
+
26
+ _HANDLERS: dict[BackendType, type[Backend]] = {
27
+ BackendType.ANTHROPIC_SDK: AnthropicSdkBackend,
28
+ BackendType.CLAUDE_CLI: ClaudeCliBackend,
29
+ BackendType.GOOGLE_SDK: GoogleSdkBackend,
30
+ BackendType.OPENAI_COMPAT: OpenAICompatBackend,
31
+ }
32
+
33
+
34
+ def get(name: str, *, config: ScrConfig) -> Backend:
35
+ """Return the adapter registered for `name`.
36
+
37
+ Raises `typer.BadParameter` with a list of valid choices if the
38
+ name is unknown. "auto" must be resolved by the caller via
39
+ `resolve_auto` first — passing it here is an error.
40
+ """
41
+ bdef = config.backends.get(name)
42
+ if bdef is None:
43
+ valid = sorted(["auto", *config.backends.keys()])
44
+ raise typer.BadParameter(f"unknown backend {name!r}; expected one of: {', '.join(valid)}.")
45
+ cls = _HANDLERS.get(bdef.type)
46
+ if cls is None:
47
+ raise typer.BadParameter(f"backend {name!r} has unknown type {bdef.type!r}")
48
+ return cls(name, bdef)
49
+
50
+
51
+ def resolve_auto(*, config: ScrConfig) -> str:
52
+ """Walk registered backends and return the first name that can satisfy auto.
53
+
54
+ Determinism: candidates are sorted by `(auto_priority, name)`.
55
+ Each candidate's `supports_auto()` is evaluated only once, in
56
+ that order.
57
+ """
58
+ candidates: list[tuple[int, str]] = []
59
+ for name, bdef in config.backends.items():
60
+ cls = _HANDLERS.get(bdef.type)
61
+ if cls is None:
62
+ continue
63
+ priority = cls.auto_priority
64
+ if priority is None:
65
+ continue
66
+ adapter = cls(name, bdef)
67
+ if adapter.supports_auto():
68
+ candidates.append((priority, name))
69
+ if not candidates:
70
+ raise typer.BadParameter(
71
+ "No Anthropic credentials available: set ANTHROPIC_API_KEY "
72
+ "(or ANTHROPIC_API_TOKEN in .env), install the `claude` CLI "
73
+ "for subscription-based fallback, or pass --backend=gemini-api "
74
+ "(Google SDK) to opt into a Gemini backend."
75
+ )
76
+ candidates.sort()
77
+ return candidates[0][1]
78
+
79
+
80
+ def register_handler(btype: BackendType, cls: type[Backend]) -> None:
81
+ """Register or replace the adapter class for a backend type.
82
+
83
+ Intended for tests that want to swap a real adapter for a stub
84
+ (e.g. avoid touching the network in `resolve_auto`). Production
85
+ code should not call this.
86
+ """
87
+ _HANDLERS[btype] = cls
88
+
89
+
90
+ __all__ = [
91
+ "Backend",
92
+ "get",
93
+ "register_handler",
94
+ "resolve_auto",
95
+ ]