-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexport.ts
More file actions
152 lines (146 loc) Β· 4.67 KB
/
Copy pathexport.ts
File metadata and controls
152 lines (146 loc) Β· 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Review export: extracts the annotated code range and builds the Markdown
// artifact. Pure β no TUI or filesystem side effects (the caller writes the
// file and reports status).
import { type FileDiff } from "./git.ts"
import {
type Annotation,
type Severity,
type Status,
type Source,
parseCommentKey,
} from "./model.ts"
export const getRangeContent = (
fd: FileDiff,
startLine: number,
endLine: number,
side: "old" | "new" = "new",
): string => {
const result: string[] = []
let newCounter = 0
let oldCounter = 0
let inHunk = false
for (const l of fd.lines) {
if (l.startsWith("@@ ")) {
const m = l.match(/@@ -(\d+)(?:,\d+)? \+(\d+)/)
if (m) {
oldCounter = parseInt(m[1]!, 10)
newCounter = parseInt(m[2]!, 10)
}
inHunk = true
} else if (!inHunk) {
// Skip the file header (diff --git, index, --- a/, +++ b/) that precedes
// the first hunk β otherwise its lines are miscounted as content.
continue
} else if (l.startsWith("+") && side === "new") {
if (newCounter >= startLine && newCounter <= endLine)
result.push(l.slice(1))
if (newCounter > endLine) break
newCounter++
} else if (l.startsWith("-") && side === "old") {
if (oldCounter >= startLine && oldCounter <= endLine)
result.push(l.slice(1))
if (oldCounter > endLine) break
oldCounter++
} else if (!l.startsWith("+") && !l.startsWith("-") && l) {
const counter = side === "old" ? oldCounter : newCounter
if (counter >= startLine && counter <= endLine) result.push(l.slice(1))
if (counter > endLine) break
newCounter++
oldCounter++
}
}
return result.join("\n")
}
export const buildReviewMarkdown = (
comments: Map<string, Annotation>,
fileDiffs: FileDiff[],
prompt: string,
): string => {
const byFile = new Map<
string,
Array<{
startLine: number
endLine: number
side: "old" | "new"
ann: Annotation
}>
>()
for (const [key, ann] of comments) {
const { file, side, startLine, endLine } = parseCommentKey(key)
if (!byFile.has(file)) byFile.set(file, [])
byFile.get(file)!.push({ startLine, endLine, side, ann })
}
const reviewSections = [
`# Code Review\n\nGenerated by revu on ${new Date().toISOString()}\n`,
]
for (const [file, entries] of byFile) {
reviewSections.push(`## \`${file}\`\n`)
const fd = fileDiffs.find((f) => f.file === file)
const ext = file.split(".").pop()?.toLowerCase() ?? ""
for (const { startLine, endLine, side, ann } of entries.sort(
(a, b) => a.startLine - b.startLine,
)) {
const content = fd ? getRangeContent(fd, startLine, endLine, side) : ""
const codeBlock = content ? `\`\`\`${ext}\n${content}\n\`\`\`\n` : ""
const sideLabel = side === "old" ? " (removed)" : ""
const statusLabel =
ann.status && ann.status !== "open" ? ` β _${ann.status}_` : ""
const lineRef =
startLine === endLine
? `Line ${startLine}${sideLabel}`
: `Lines ${startLine}β${endLine}${sideLabel}`
// Conventional-comment prefix driven by severity (blocker/concern/nitpick).
const prefix = ann.severity ? `**${ann.severity}:** ` : ""
const commentText = (prefix + ann.text)
.split("\n")
.map((l) => `> ${l}`)
.join("\n")
reviewSections.push(
`### ${lineRef}${statusLabel}\n${codeBlock}${commentText}\n`,
)
}
}
const header = prompt.trim() ? `${prompt.trim()}\n\n---\n\n` : ""
return header + reviewSections.join("\n")
}
// Machine-readable review for agents/tooling: one entry per annotation with
// resolved triage fields and the captured code range.
export interface JsonReviewComment {
file: string
startLine: number
endLine: number
side: "old" | "new"
severity: Severity | null
status: Status
source: Source
text: string
code: string
}
export interface JsonReview {
prompt: string
comments: JsonReviewComment[]
}
export const buildReviewJson = (
comments: Map<string, Annotation>,
fileDiffs: FileDiff[],
prompt: string,
): JsonReview => {
const out: JsonReviewComment[] = []
for (const [key, ann] of comments) {
const { file, side, startLine, endLine } = parseCommentKey(key)
const fd = fileDiffs.find((f) => f.file === file)
out.push({
file,
startLine,
endLine,
side,
severity: ann.severity ?? null,
status: ann.status ?? "open",
source: ann.source ?? "human",
text: ann.text,
code: fd ? getRangeContent(fd, startLine, endLine, side) : "",
})
}
out.sort((a, b) => a.file.localeCompare(b.file) || a.startLine - b.startLine)
return { prompt, comments: out }
}