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.
- semantic_code_review/__init__.py +5 -0
- semantic_code_review/augment/__init__.py +3 -0
- semantic_code_review/augment/agents.py +95 -0
- semantic_code_review/augment/console.py +440 -0
- semantic_code_review/augment/extra_review.py +237 -0
- semantic_code_review/augment/fold_summary.py +447 -0
- semantic_code_review/augment/hunks.py +264 -0
- semantic_code_review/augment/mcp_server.py +156 -0
- semantic_code_review/augment/overview.py +233 -0
- semantic_code_review/augment/pass_.py +171 -0
- semantic_code_review/augment/pipeline.py +571 -0
- semantic_code_review/augment/progress.py +223 -0
- semantic_code_review/augment/prompts.py +93 -0
- semantic_code_review/augment/schemas.py +408 -0
- semantic_code_review/augment/tools.py +475 -0
- semantic_code_review/augment/trace_adapter.py +369 -0
- semantic_code_review/backends/__init__.py +95 -0
- semantic_code_review/backends/_cli_driver.py +571 -0
- semantic_code_review/backends/anthropic_sdk.py +56 -0
- semantic_code_review/backends/base.py +104 -0
- semantic_code_review/backends/claude_cli.py +421 -0
- semantic_code_review/backends/google_sdk.py +73 -0
- semantic_code_review/backends/openai_compat.py +42 -0
- semantic_code_review/cache/__init__.py +3 -0
- semantic_code_review/cache/store.py +87 -0
- semantic_code_review/cli/__init__.py +70 -0
- semantic_code_review/cli/_shared.py +157 -0
- semantic_code_review/cli/augment.py +74 -0
- semantic_code_review/cli/config_cmd.py +266 -0
- semantic_code_review/cli/fetch.py +33 -0
- semantic_code_review/cli/lint.py +27 -0
- semantic_code_review/cli/pr.py +98 -0
- semantic_code_review/cli/review.py +104 -0
- semantic_code_review/cli/runs_cmd.py +17 -0
- semantic_code_review/cli/show.py +19 -0
- semantic_code_review/cli/strip.py +20 -0
- semantic_code_review/config.py +563 -0
- semantic_code_review/config_template.py +154 -0
- semantic_code_review/fetch/__init__.py +61 -0
- semantic_code_review/fetch/anchor.py +244 -0
- semantic_code_review/fetch/github.py +229 -0
- semantic_code_review/fetch/github_comments.py +404 -0
- semantic_code_review/fetch/local.py +443 -0
- semantic_code_review/fetch/run_source.py +70 -0
- semantic_code_review/format/__init__.py +3 -0
- semantic_code_review/format/emit.py +163 -0
- semantic_code_review/format/lint.py +74 -0
- semantic_code_review/format/parse.py +574 -0
- semantic_code_review/format/sidecar.py +19 -0
- semantic_code_review/format/strip.py +20 -0
- semantic_code_review/git_ops.py +252 -0
- semantic_code_review/paths.py +61 -0
- semantic_code_review/review/__init__.py +3 -0
- semantic_code_review/review/comments.py +176 -0
- semantic_code_review/review/github.py +284 -0
- semantic_code_review/review/github_graphql.py +433 -0
- semantic_code_review/review/pr_flow.py +299 -0
- semantic_code_review/review/runner.py +407 -0
- semantic_code_review/review/server.py +1043 -0
- semantic_code_review/structural/__init__.py +41 -0
- semantic_code_review/structural/diff.py +102 -0
- semantic_code_review/structural/parse.py +330 -0
- semantic_code_review/structural/symbols.py +43 -0
- semantic_code_review/viewer/__init__.py +3 -0
- semantic_code_review/viewer/assets/annotations.ts +547 -0
- semantic_code_review/viewer/assets/boot.ts +273 -0
- semantic_code_review/viewer/assets/comment_store.ts +121 -0
- semantic_code_review/viewer/assets/comments.ts +624 -0
- semantic_code_review/viewer/assets/console.ts +426 -0
- semantic_code_review/viewer/assets/console_render.ts +213 -0
- semantic_code_review/viewer/assets/console_selection.ts +102 -0
- semantic_code_review/viewer/assets/data_store.ts +156 -0
- semantic_code_review/viewer/assets/file_rows.ts +49 -0
- semantic_code_review/viewer/assets/folds.ts +560 -0
- semantic_code_review/viewer/assets/index.html +62 -0
- semantic_code_review/viewer/assets/post_modal.ts +383 -0
- semantic_code_review/viewer/assets/progress.ts +138 -0
- semantic_code_review/viewer/assets/render.ts +937 -0
- semantic_code_review/viewer/assets/sidebar.ts +429 -0
- semantic_code_review/viewer/assets/sse.ts +78 -0
- semantic_code_review/viewer/assets/text_highlight.ts +215 -0
- semantic_code_review/viewer/assets/types.d.ts +388 -0
- semantic_code_review/viewer/assets/vendor/LICENSE +29 -0
- semantic_code_review/viewer/assets/vendor/VENDOR.md +55 -0
- semantic_code_review/viewer/assets/vendor/github-dark.min.css +10 -0
- semantic_code_review/viewer/assets/vendor/github.min.css +10 -0
- semantic_code_review/viewer/assets/vendor/highlight.min.js +1244 -0
- semantic_code_review/viewer/assets/vendor/mermaid.LICENSE +21 -0
- semantic_code_review/viewer/assets/vendor/mermaid.min.js +3587 -0
- semantic_code_review/viewer/assets/vendor/refresh.sh +65 -0
- semantic_code_review/viewer/assets/viewer.css +1202 -0
- semantic_code_review/viewer/assets/viewer.js +10588 -0
- semantic_code_review/viewer/build_json.py +546 -0
- semantic_code_review/viewer/hunk_layout.py +471 -0
- semantic_code_review-0.24.2.dist-info/METADATA +348 -0
- semantic_code_review-0.24.2.dist-info/RECORD +100 -0
- semantic_code_review-0.24.2.dist-info/WHEEL +5 -0
- semantic_code_review-0.24.2.dist-info/entry_points.txt +2 -0
- semantic_code_review-0.24.2.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|