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,408 @@
1
+ """Pydantic models for the augmented-diff annotations.
2
+
3
+ These types are the canonical structured representation. `format.emit`
4
+ writes them out as an augmented unified diff; `format.parse` reads them
5
+ back. The same types flow into the viewer JSON bundle.
6
+
7
+ Two stages exist as distinct types:
8
+
9
+ - `ParsedDiff` / `ParsedFile` / `ParsedHunk` — what comes out of parsing
10
+ a raw git diff: structure only, no annotations.
11
+ - `AnnotatedDiff` / `AnnotatedFile` / `AnnotatedHunk` — pairs the parsed
12
+ structure with annotation payloads (`Overview | SkippedOverview` at the
13
+ diff root, `FileAnnotations` per file, `HunkAnnotations` per hunk).
14
+
15
+ `emit_augmented_diff` requires the annotated form. The augment pipeline
16
+ takes a `ParsedDiff` and produces an `AnnotatedDiff` via pure functions.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from enum import Enum
22
+ from typing import Literal
23
+
24
+ from pydantic import BaseModel, ConfigDict, Field
25
+
26
+
27
+ class Severity(str, Enum):
28
+ INFO = "info"
29
+ MINOR = "minor"
30
+ MAJOR = "major"
31
+ CRITICAL = "critical"
32
+
33
+
34
+ class SmellDef(BaseModel):
35
+ """Entry in the closed smell vocabulary (plan §3.4)."""
36
+
37
+ tag: str
38
+ label: str
39
+ severity: Severity
40
+ color: str
41
+
42
+
43
+ SMELL_CATALOGUE: dict[str, SmellDef] = {
44
+ d.tag: d
45
+ for d in (
46
+ SmellDef(tag="duplication", label="Duplicated logic", severity=Severity.MINOR, color="#888"),
47
+ SmellDef(tag="string-sql", label="SQL built by string", severity=Severity.MAJOR, color="#e60"),
48
+ SmellDef(tag="no-input-validation", label="No input validation", severity=Severity.MAJOR, color="#e60"),
49
+ SmellDef(tag="missing-test", label="No test coverage", severity=Severity.MAJOR, color="#e60"),
50
+ SmellDef(tag="security-sensitive", label="Security-sensitive", severity=Severity.CRITICAL, color="#c33"),
51
+ SmellDef(tag="performance-regression", label="Performance regression", severity=Severity.MAJOR, color="#e60"),
52
+ SmellDef(tag="backward-incompatible", label="Backward-incompatible", severity=Severity.CRITICAL, color="#c33"),
53
+ SmellDef(tag="todo-left-behind", label="TODO left in code", severity=Severity.INFO, color="#678"),
54
+ SmellDef(tag="dead-code", label="Dead code", severity=Severity.MINOR, color="#888"),
55
+ SmellDef(tag="unscoped-exception", label="Broad exception handler", severity=Severity.MINOR, color="#888"),
56
+ SmellDef(tag="resource-leak", label="Resource leak", severity=Severity.MAJOR, color="#e60"),
57
+ SmellDef(tag="race-condition", label="Race condition", severity=Severity.CRITICAL, color="#c33"),
58
+ )
59
+ }
60
+
61
+ SMELL_TAGS: frozenset[str] = frozenset(SMELL_CATALOGUE)
62
+ # Insertion-ordered comma-separated list, suitable for embedding in
63
+ # prompt text and JSON-schema descriptions. SMELL_TAGS (frozenset) is
64
+ # the right thing for membership checks; SMELL_TAGS_TEXT is the right
65
+ # thing for "tell the model the closed vocabulary".
66
+ SMELL_TAGS_TEXT: str = ", ".join(SMELL_CATALOGUE.keys())
67
+
68
+
69
+ class Smell(BaseModel):
70
+ tag: str = Field(description=f"One of: {SMELL_TAGS_TEXT}")
71
+ note: str = ""
72
+
73
+
74
+ class Ref(BaseModel):
75
+ path: str
76
+ line: int
77
+ reason: str = ""
78
+
79
+
80
+ class LineNote(BaseModel):
81
+ line: int
82
+ body: str
83
+
84
+
85
+ class Segment(BaseModel):
86
+ new_start: int
87
+ new_count: int
88
+ intent: str = ""
89
+ smells: list[Smell] = Field(default_factory=list)
90
+ context: str = ""
91
+ refs: list[Ref] = Field(default_factory=list)
92
+
93
+
94
+ class FoldDescription(BaseModel):
95
+ """One-line summary of the body inside an indent fold region.
96
+
97
+ Addressed by 1-indexed line ranges into the *files* the diff
98
+ relates — never into the hunk's row sequence — so the
99
+ representation is stable across re-renders and lets folds span
100
+ expanded context as well as hunk bodies.
101
+
102
+ `context` picks which side(s) the fold covers:
103
+ - "right": post-image lines only (the common case — describe
104
+ what the new code does). Address with right_start/right_end
105
+ as 1-indexed line numbers in head/<path>.
106
+ - "left": pre-image lines only (a pure deletion fold).
107
+ Address with left_start/left_end in base/<path>.
108
+ - "both": fold straddles changed content. Both ranges populated.
109
+
110
+ Generated lazily by the review server's `/fold-summary` route
111
+ the first time the reviewer collapses a region; cached so
112
+ subsequent reviews skip the call.
113
+ """
114
+
115
+ context: Literal["right", "left", "both"] = "right"
116
+ right_start: int = 0
117
+ right_end: int = 0
118
+ left_start: int = 0
119
+ left_end: int = 0
120
+ summary: str
121
+
122
+
123
+ class FileRole(str, Enum):
124
+ NEW = "new"
125
+ DELETED = "deleted"
126
+ RENAMED = "renamed"
127
+ MODIFIED = "modified"
128
+ BINARY = "binary"
129
+ GENERATED = "generated"
130
+
131
+
132
+ class FileSymbols(BaseModel):
133
+ model_config = ConfigDict(populate_by_name=True)
134
+
135
+ added: list[str] = Field(default_factory=list)
136
+ modified: list[str] = Field(default_factory=list)
137
+ removed: list[str] = Field(default_factory=list)
138
+
139
+
140
+ class OverviewSymbol(BaseModel):
141
+ path: str
142
+ kind: str
143
+ name: str
144
+
145
+
146
+ class OverviewEdge(BaseModel):
147
+ src: str = Field(alias="from")
148
+ dst: str = Field(alias="to")
149
+
150
+ model_config = ConfigDict(populate_by_name=True)
151
+
152
+
153
+ class OverviewGroupMember(BaseModel):
154
+ """A hunk-address the LLM returned as part of a semantic group.
155
+
156
+ `path` is the file's post-image path; `hunk_index` is the 0-based
157
+ offset into that file's hunk list. Invalid references (path not
158
+ present, or index out of range) are dropped at parse time by
159
+ `apply_overview_to_diff`.
160
+ """
161
+
162
+ path: str
163
+ hunk_index: int
164
+
165
+
166
+ class OverviewGroup(BaseModel):
167
+ """A cluster of hunks the LLM believes share a purpose.
168
+
169
+ Hunks may appear in multiple groups (overlap is expected — one
170
+ hunk can serve two themes) and need not cover every hunk in the
171
+ diff. The viewer renders each group as a sidebar entry; clicking
172
+ one filters the visible hunks to that group's members.
173
+ """
174
+
175
+ title: str
176
+ rationale: str = ""
177
+ members: list[OverviewGroupMember] = Field(default_factory=list)
178
+
179
+
180
+ class Overview(BaseModel):
181
+ summary: str = ""
182
+ symbols_added: list[OverviewSymbol] = Field(default_factory=list)
183
+ symbols_modified: list[OverviewSymbol] = Field(default_factory=list)
184
+ symbols_removed: list[OverviewSymbol] = Field(default_factory=list)
185
+ callgraph_edges: list[OverviewEdge] = Field(default_factory=list)
186
+ themes: list[str] = Field(default_factory=list)
187
+ groups: list[OverviewGroup] = Field(default_factory=list)
188
+
189
+
190
+ class SkippedOverview(BaseModel):
191
+ """Sentinel for `AnnotatedDiff.overview` when the overview pass was
192
+ not run (or was skipped via `--no-overview`).
193
+
194
+ Distinct from `Overview()` (the pass ran and produced empty fields):
195
+ the viewer can render "no overview was generated" differently from
196
+ "the overview is intentionally empty".
197
+ """
198
+
199
+
200
+ class PRInfo(BaseModel):
201
+ pr_url: str
202
+ base_sha: str
203
+ head_sha: str
204
+ model: str = ""
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Stage 1 — Parsed structure (no annotations).
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ class ParsedHunk(BaseModel):
213
+ """One hunk's structural state as it came off the diff stream."""
214
+
215
+ header: str
216
+ old_start: int
217
+ old_count: int
218
+ new_start: int
219
+ new_count: int
220
+ body: str = ""
221
+
222
+
223
+ class ParsedFile(BaseModel):
224
+ """One file's section of the unified diff (no annotations)."""
225
+
226
+ path: str
227
+ old_path: str | None = None
228
+ diff_git_line: str
229
+ extra_header_lines: list[str] = Field(default_factory=list)
230
+ old_file_marker: str = ""
231
+ new_file_marker: str = ""
232
+ hunks: list[ParsedHunk] = Field(default_factory=list)
233
+
234
+
235
+ class ParsedDiff(BaseModel):
236
+ version: int = 1
237
+ pr: PRInfo
238
+ files: list[ParsedFile] = Field(default_factory=list)
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Annotation payloads — what the LLM emits per pass.
243
+ #
244
+ # `OverviewSubmission` and `HunkAnnotations` are the wire format of
245
+ # `submit_overview` / `submit_annotations`. Their JSON schemas (via
246
+ # `model_json_schema`) replace the hand-written tool input_schemas in
247
+ # `prompts.py`, giving us one source of truth for "what we ask the model
248
+ # for" and "how we parse it back".
249
+ # ---------------------------------------------------------------------------
250
+
251
+
252
+ class HunkAnnotations(BaseModel):
253
+ """Wire format of `submit_annotations` and the per-hunk annotation
254
+ block carried on `AnnotatedHunk.ann`.
255
+ """
256
+
257
+ intent: str = Field(description="1-2 sentences of MOTIVE, not mechanics.")
258
+ segments: list[Segment] = Field(
259
+ default_factory=list,
260
+ description=(
261
+ "Split the hunk into semantically distinct edits when present "
262
+ "(e.g. a refactor plus an unrelated fix). Each segment carries "
263
+ "post-image new_start/new_count and its own intent. Omit if "
264
+ "the hunk is single-intent."
265
+ ),
266
+ )
267
+ smells: list[Smell] = Field(default_factory=list)
268
+ context: str = Field(
269
+ default="",
270
+ description="Cross-file dependencies the reviewer can't see from the diff.",
271
+ )
272
+ refs: list[Ref] = Field(default_factory=list)
273
+ confidence: int | None = Field(default=None, ge=0, le=100)
274
+ line_notes: list[LineNote] = Field(default_factory=list)
275
+ fold_descriptions: list[FoldDescription] = Field(
276
+ default_factory=list,
277
+ description=(
278
+ "One short sentence per indent fold region containing changes. "
279
+ "Match each region's new_start/new_count exactly."
280
+ ),
281
+ )
282
+
283
+
284
+ class FileAnnotations(BaseModel):
285
+ """Per-file annotations carried on `AnnotatedFile.ann`."""
286
+
287
+ role: FileRole | None = None
288
+ summary: str = ""
289
+ lang: str | None = None
290
+ symbols: FileSymbols | None = None
291
+
292
+
293
+ class OverviewFileSubmission(BaseModel):
294
+ """Per-file annotations the LLM submits as part of the overview pass."""
295
+
296
+ path: str
297
+ summary: str = Field(default="", description="One sentence per file.")
298
+ lang: str | None = Field(default=None, description="Only when the extension is ambiguous.")
299
+ symbols: FileSymbols | None = None
300
+
301
+
302
+ class OverviewSubmission(BaseModel):
303
+ """Wire format of `submit_overview`. Consumed by `apply_overview_to_diff`."""
304
+
305
+ summary: str = Field(description="1-3 sentence summary of the PR's intent.")
306
+ symbols_added: list[OverviewSymbol] = Field(default_factory=list)
307
+ symbols_modified: list[OverviewSymbol] = Field(default_factory=list)
308
+ symbols_removed: list[OverviewSymbol] = Field(default_factory=list)
309
+ callgraph_edges: list[OverviewEdge] = Field(
310
+ default_factory=list,
311
+ description="Introduced or modified calls (best-effort — omit if unsure).",
312
+ )
313
+ themes: list[str] = Field(
314
+ default_factory=list,
315
+ description="Short keyword tags (e.g. 'pagination', 'api-surface').",
316
+ )
317
+ files: list[OverviewFileSubmission] = Field(
318
+ default_factory=list,
319
+ description="Per-file summaries; one entry per changed file in the diff.",
320
+ )
321
+ groups: list[OverviewGroup] = Field(
322
+ default_factory=list,
323
+ description=(
324
+ "Semantic clusters of hunks the reviewer can filter by. Each "
325
+ "hunk may appear in 0+ groups — overlap is expected when a hunk "
326
+ "serves multiple purposes. Aim for 2–6 groups on a typical PR "
327
+ "(more for large ones). A group need not cover every hunk; "
328
+ "leave genuinely standalone hunks out."
329
+ ),
330
+ )
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Stage 2 — Annotated form (parsed structure + annotation payloads).
335
+ # ---------------------------------------------------------------------------
336
+
337
+
338
+ def _empty_hunk_annotations() -> HunkAnnotations:
339
+ return HunkAnnotations(intent="")
340
+
341
+
342
+ class AnnotatedHunk(BaseModel):
343
+ """A parsed hunk paired with its annotation payload."""
344
+
345
+ parsed: ParsedHunk
346
+ ann: HunkAnnotations = Field(default_factory=_empty_hunk_annotations)
347
+
348
+
349
+ class AnnotatedFile(BaseModel):
350
+ """A parsed file paired with its annotation payload and annotated hunks.
351
+
352
+ Structural fields are flat (mirroring `ParsedFile`) so callers can
353
+ write `f.path` rather than `f.parsed.path`. The hunks list carries
354
+ `AnnotatedHunk` items, which themselves wrap a `ParsedHunk`.
355
+ """
356
+
357
+ path: str
358
+ old_path: str | None = None
359
+ diff_git_line: str
360
+ extra_header_lines: list[str] = Field(default_factory=list)
361
+ old_file_marker: str = ""
362
+ new_file_marker: str = ""
363
+ ann: FileAnnotations = Field(default_factory=FileAnnotations)
364
+ hunks: list[AnnotatedHunk] = Field(default_factory=list)
365
+
366
+
367
+ class AnnotatedDiff(BaseModel):
368
+ version: int = 1
369
+ pr: PRInfo
370
+ overview: Overview | SkippedOverview = Field(default_factory=SkippedOverview)
371
+ files: list[AnnotatedFile] = Field(default_factory=list)
372
+
373
+
374
+ # ---------------------------------------------------------------------------
375
+ # Lifting helpers — wrap parsed values in annotated form with empty defaults.
376
+ # ---------------------------------------------------------------------------
377
+
378
+
379
+ def lift_hunk(parsed: ParsedHunk, ann: HunkAnnotations | None = None) -> AnnotatedHunk:
380
+ return AnnotatedHunk(parsed=parsed, ann=ann or _empty_hunk_annotations())
381
+
382
+
383
+ def lift_file(
384
+ parsed: ParsedFile,
385
+ *,
386
+ ann: FileAnnotations | None = None,
387
+ hunks: list[AnnotatedHunk] | None = None,
388
+ ) -> AnnotatedFile:
389
+ return AnnotatedFile(
390
+ path=parsed.path,
391
+ old_path=parsed.old_path,
392
+ diff_git_line=parsed.diff_git_line,
393
+ extra_header_lines=list(parsed.extra_header_lines),
394
+ old_file_marker=parsed.old_file_marker,
395
+ new_file_marker=parsed.new_file_marker,
396
+ ann=ann or FileAnnotations(),
397
+ hunks=hunks if hunks is not None else [lift_hunk(h) for h in parsed.hunks],
398
+ )
399
+
400
+
401
+ def lift_diff(parsed: ParsedDiff) -> AnnotatedDiff:
402
+ """Lift a `ParsedDiff` into an `AnnotatedDiff` with empty annotations."""
403
+ return AnnotatedDiff(
404
+ version=parsed.version,
405
+ pr=parsed.pr,
406
+ overview=SkippedOverview(),
407
+ files=[lift_file(f) for f in parsed.files],
408
+ )