lody 0.62.0 → 0.63.0
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.
- package/dist/chunks/diff-line-counts-BLxwWP6r.js +675 -0
- package/dist/chunks/embedded-prompt-D__ujYMd.js +4 -0
- package/dist/chunks/index-B9fKl40V.js +308 -0
- package/dist/chunks/index-CUUne095.js +33 -0
- package/dist/chunks/index-v46gaGAl.js +17 -0
- package/dist/chunks/loro_wasm_bg-N-tMVpou.js +5197 -0
- package/dist/chunks/review-viewer-C61Z0Yud.js +80 -0
- package/dist/chunks/sparse-text-D7zcV2O5.js +649 -0
- package/dist/chunks/turn-diff-replay-qRat9u1k.js +311 -0
- package/dist/diff-worker.js +11 -0
- package/dist/index.js +13382 -14116
- package/dist/turn-diff-replay-worker.js +16 -0
- package/package.json +15 -12
- package/dist/chunks/index-DekwrpNR.js +0 -449
- package/dist/chunks/index-Dr2JkTB2.js +0 -10121
- package/dist/chunks/loro_wasm_bg-BV-n7JyC.js +0 -5198
- package/dist/chunks/share-link-U1EVLOdF.js +0 -974
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
const FRONTMATTER_DELIMITER = "---";
|
|
2
|
+
const DEFAULT_LINE_BUDGET = 1500;
|
|
3
|
+
const GROUP_HEADING = /^##\s+Group:\s*(.+?)\s*$/u;
|
|
4
|
+
const CHANGED_LINES = /^Changed lines:\s*(\d+)\s*$/iu;
|
|
5
|
+
const COMMITS = /^Commits:\s*(.*?)\s*$/iu;
|
|
6
|
+
const CODE_SPAN = /`([^`]+)`/gu;
|
|
7
|
+
const CHANGE_REF = /^`(changes:\/\/[^`]+)`\s*$/u;
|
|
8
|
+
const CONTEXT_REF = /^`(context:\/\/[^`]+)`\s*$/u;
|
|
9
|
+
const NOTE_REF = /^\s*-\s*(?:(P0|P1|P2|QUESTION|INFO|WARNING|ERROR)\s+)?`(old|new):\/\/([^`]+)`\s*[::]\s*(.+?)\s*$/iu;
|
|
10
|
+
const REVIEW_HEADING = /^##\s+Review\s*$/iu;
|
|
11
|
+
const FINDING_ENTRY = /^\s*-\s*(P0|P1|P2|QUESTION|INFO|WARNING|ERROR)\b\s*[::]\s*(.*)$/iu;
|
|
12
|
+
const REVIEW_REF_SCHEME = /^(old|new):\/\/(.+?)(?::L(\d+)(?:-L?(\d+))?)?$/iu;
|
|
13
|
+
const REVIEW_REF_BARE_PATH = /^(?:[\w.@~-]+\/)+[\w.@~-]+\.[A-Za-z0-9]+$/u;
|
|
14
|
+
function parseReviewRef(token) {
|
|
15
|
+
const trimmed = token.trim();
|
|
16
|
+
const scheme = REVIEW_REF_SCHEME.exec(trimmed);
|
|
17
|
+
if (scheme) {
|
|
18
|
+
const path = (scheme[2] ?? "").trim();
|
|
19
|
+
if (path.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const side = (scheme[1] ?? "new").toLowerCase();
|
|
23
|
+
const startRaw = scheme[3];
|
|
24
|
+
let range;
|
|
25
|
+
if (startRaw !== void 0) {
|
|
26
|
+
const start = Number(startRaw);
|
|
27
|
+
const end = scheme[4] === void 0 ? start : Number(scheme[4]);
|
|
28
|
+
if (Number.isInteger(start) && Number.isInteger(end) && start >= 1 && end >= start) {
|
|
29
|
+
range = { start, end };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { path, side, ...range ? { range } : {}, raw: trimmed };
|
|
33
|
+
}
|
|
34
|
+
if (REVIEW_REF_BARE_PATH.test(trimmed)) {
|
|
35
|
+
return { path: trimmed, side: "new", raw: trimmed };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function extractReviewRefs(text) {
|
|
40
|
+
const refs = [];
|
|
41
|
+
for (const match of text.matchAll(CODE_SPAN)) {
|
|
42
|
+
const ref = parseReviewRef(match[1] ?? "");
|
|
43
|
+
if (ref) {
|
|
44
|
+
refs.push(ref);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return refs;
|
|
48
|
+
}
|
|
49
|
+
function parseReviewFindings(lines, baseLineNumber) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
let current;
|
|
52
|
+
const flush = () => {
|
|
53
|
+
if (!current) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const bodyMarkdown = trimOuterBlankLines(current.bodyLines).join("\n").trim();
|
|
57
|
+
findings.push({
|
|
58
|
+
id: `finding-${findings.length + 1}`,
|
|
59
|
+
severity: current.severity,
|
|
60
|
+
bodyMarkdown,
|
|
61
|
+
refs: extractReviewRefs(bodyMarkdown),
|
|
62
|
+
line: current.line
|
|
63
|
+
});
|
|
64
|
+
current = void 0;
|
|
65
|
+
};
|
|
66
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
67
|
+
const rawLine = lines[index] ?? "";
|
|
68
|
+
const entry = FINDING_ENTRY.exec(rawLine);
|
|
69
|
+
if (entry) {
|
|
70
|
+
flush();
|
|
71
|
+
current = {
|
|
72
|
+
severity: normalizeNoteSeverity(entry[1]),
|
|
73
|
+
bodyLines: [entry[2] ?? ""],
|
|
74
|
+
line: baseLineNumber + index
|
|
75
|
+
};
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (current) {
|
|
79
|
+
current.bodyLines.push(rawLine);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
flush();
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
85
|
+
function normalizeNoteSeverity(token) {
|
|
86
|
+
switch (token?.toUpperCase()) {
|
|
87
|
+
case "P0":
|
|
88
|
+
case "ERROR":
|
|
89
|
+
return "p0";
|
|
90
|
+
case "P1":
|
|
91
|
+
case "WARNING":
|
|
92
|
+
return "p1";
|
|
93
|
+
case "P2":
|
|
94
|
+
return "p2";
|
|
95
|
+
case "QUESTION":
|
|
96
|
+
return "question";
|
|
97
|
+
default:
|
|
98
|
+
return "info";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function parseReviewMarkdown(markdown, options = {}) {
|
|
102
|
+
const diagnostics = [];
|
|
103
|
+
const normalized = markdown.replace(/\r\n?/gu, "\n");
|
|
104
|
+
const lines = normalized.split("\n");
|
|
105
|
+
const { frontmatter, bodyStartIndex } = parseFrontmatter(lines, diagnostics);
|
|
106
|
+
const { groups, title, overview, findings } = parseGroups(lines, bodyStartIndex, diagnostics);
|
|
107
|
+
if (title === void 0) {
|
|
108
|
+
diagnostics.push({
|
|
109
|
+
severity: "warning",
|
|
110
|
+
message: 'Review should start the overview with a "# Title" heading.',
|
|
111
|
+
line: bodyStartIndex + 1,
|
|
112
|
+
code: "missing_review_title"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (groups.length === 0) {
|
|
116
|
+
diagnostics.push({
|
|
117
|
+
severity: "error",
|
|
118
|
+
message: 'Review file must contain at least one "## Group: ..." section.',
|
|
119
|
+
code: "missing_group"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...options.sourcePath === void 0 ? {} : { sourcePath: options.sourcePath },
|
|
124
|
+
frontmatter,
|
|
125
|
+
...title === void 0 ? {} : { title },
|
|
126
|
+
...overview === void 0 ? {} : { overview },
|
|
127
|
+
findings,
|
|
128
|
+
groups,
|
|
129
|
+
diagnostics
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function parseFrontmatter(lines, diagnostics) {
|
|
133
|
+
if (lines[0]?.trim() !== FRONTMATTER_DELIMITER) {
|
|
134
|
+
diagnostics.push({
|
|
135
|
+
severity: "error",
|
|
136
|
+
message: 'Review file must start with YAML frontmatter delimited by "---".',
|
|
137
|
+
line: 1,
|
|
138
|
+
code: "missing_frontmatter"
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
frontmatter: fallbackFrontmatter({}),
|
|
142
|
+
bodyStartIndex: 0
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const raw = {};
|
|
146
|
+
let endIndex = -1;
|
|
147
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
148
|
+
const line = lines[index];
|
|
149
|
+
if (line?.trim() === FRONTMATTER_DELIMITER) {
|
|
150
|
+
endIndex = index;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
const parsed = /^([A-Za-z0-9_-]+):\s*(.*?)\s*$/u.exec(line ?? "");
|
|
154
|
+
if (!parsed) {
|
|
155
|
+
diagnostics.push({
|
|
156
|
+
severity: "warning",
|
|
157
|
+
message: `Ignoring unsupported frontmatter line: ${line ?? ""}`,
|
|
158
|
+
line: index + 1,
|
|
159
|
+
code: "unsupported_frontmatter_line"
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
raw[parsed[1] ?? ""] = stripYamlScalar(parsed[2] ?? "");
|
|
164
|
+
}
|
|
165
|
+
if (endIndex === -1) {
|
|
166
|
+
diagnostics.push({
|
|
167
|
+
severity: "error",
|
|
168
|
+
message: 'Frontmatter is missing a closing "---" delimiter.',
|
|
169
|
+
line: 1,
|
|
170
|
+
code: "unterminated_frontmatter"
|
|
171
|
+
});
|
|
172
|
+
return { frontmatter: fallbackFrontmatter(raw), bodyStartIndex: lines.length };
|
|
173
|
+
}
|
|
174
|
+
const frontmatter = fallbackFrontmatter(raw);
|
|
175
|
+
if (raw.review_version !== "1") {
|
|
176
|
+
diagnostics.push({
|
|
177
|
+
severity: "error",
|
|
178
|
+
message: "frontmatter.review_version must be 1.",
|
|
179
|
+
line: 2,
|
|
180
|
+
code: "invalid_review_version"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (!raw.merge_base) {
|
|
184
|
+
diagnostics.push({
|
|
185
|
+
severity: "error",
|
|
186
|
+
message: "frontmatter.merge_base is required.",
|
|
187
|
+
line: 2,
|
|
188
|
+
code: "missing_merge_base"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!raw.current_commit) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
severity: "error",
|
|
194
|
+
message: "frontmatter.current_commit is required.",
|
|
195
|
+
line: 2,
|
|
196
|
+
code: "missing_current_commit"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (raw.line_budget !== void 0 && !Number.isFinite(Number(raw.line_budget))) {
|
|
200
|
+
diagnostics.push({
|
|
201
|
+
severity: "warning",
|
|
202
|
+
message: `frontmatter.line_budget is not a number; using ${DEFAULT_LINE_BUDGET}.`,
|
|
203
|
+
line: 2,
|
|
204
|
+
code: "invalid_line_budget"
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return { frontmatter, bodyStartIndex: endIndex + 1 };
|
|
208
|
+
}
|
|
209
|
+
function fallbackFrontmatter(raw) {
|
|
210
|
+
const parsedBudget = Number(raw.line_budget);
|
|
211
|
+
return {
|
|
212
|
+
reviewVersion: 1,
|
|
213
|
+
mergeBase: raw.merge_base ?? "",
|
|
214
|
+
currentCommit: raw.current_commit ?? "",
|
|
215
|
+
...raw.base_ref === void 0 || raw.base_ref.length === 0 ? {} : { baseRef: raw.base_ref },
|
|
216
|
+
lineBudget: Number.isFinite(parsedBudget) && parsedBudget > 0 ? parsedBudget : DEFAULT_LINE_BUDGET,
|
|
217
|
+
pr: parsePullRequest(raw),
|
|
218
|
+
raw
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function parsePullRequest(raw) {
|
|
222
|
+
const numberRaw = raw.pr_number;
|
|
223
|
+
const urlRaw = raw.pr_url;
|
|
224
|
+
if (numberRaw === void 0 && urlRaw === void 0) {
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
const number = Number(numberRaw);
|
|
228
|
+
const url = urlRaw ?? "";
|
|
229
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
if (!isValidUrl(url)) {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
const title = raw.pr_title;
|
|
236
|
+
return {
|
|
237
|
+
number,
|
|
238
|
+
url,
|
|
239
|
+
...title === void 0 || title.length === 0 ? {} : { title }
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function stripYamlScalar(value) {
|
|
243
|
+
const trimmed = value.trim();
|
|
244
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
245
|
+
return trimmed.slice(1, -1);
|
|
246
|
+
}
|
|
247
|
+
return trimmed;
|
|
248
|
+
}
|
|
249
|
+
function parseGroups(lines, bodyStartIndex, diagnostics) {
|
|
250
|
+
const groups = [];
|
|
251
|
+
const overviewLines = [];
|
|
252
|
+
const reviewLines = [];
|
|
253
|
+
let reviewStartLine = 0;
|
|
254
|
+
let inReview = false;
|
|
255
|
+
let currentGroup;
|
|
256
|
+
let currentBlock;
|
|
257
|
+
for (let index = bodyStartIndex; index < lines.length; index += 1) {
|
|
258
|
+
const rawLine = lines[index] ?? "";
|
|
259
|
+
const lineNumber = index + 1;
|
|
260
|
+
const heading = GROUP_HEADING.exec(rawLine);
|
|
261
|
+
if (heading) {
|
|
262
|
+
inReview = false;
|
|
263
|
+
currentGroup = {
|
|
264
|
+
id: `group-${groups.length + 1}`,
|
|
265
|
+
title: heading[1]?.trim() ?? `Group ${groups.length + 1}`,
|
|
266
|
+
commits: [],
|
|
267
|
+
bodyLines: [],
|
|
268
|
+
blocks: [],
|
|
269
|
+
line: lineNumber
|
|
270
|
+
};
|
|
271
|
+
groups.push(currentGroup);
|
|
272
|
+
currentBlock = void 0;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!currentGroup && REVIEW_HEADING.test(rawLine)) {
|
|
276
|
+
inReview = true;
|
|
277
|
+
reviewStartLine = lineNumber + 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (!currentGroup) {
|
|
281
|
+
(inReview ? reviewLines : overviewLines).push(rawLine);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const changedLines = CHANGED_LINES.exec(rawLine);
|
|
285
|
+
if (changedLines) {
|
|
286
|
+
currentGroup.changedLines = Number(changedLines[1]);
|
|
287
|
+
currentGroup.bodyLines.push(rawLine);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const commits = COMMITS.exec(rawLine);
|
|
291
|
+
if (commits) {
|
|
292
|
+
currentGroup.commits = parseCommits(commits[1] ?? "");
|
|
293
|
+
currentGroup.bodyLines.push(rawLine);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const changeRef = CHANGE_REF.exec(rawLine.trim());
|
|
297
|
+
if (changeRef) {
|
|
298
|
+
const parsedRef = parseChangeReference(changeRef[1] ?? "");
|
|
299
|
+
if (!parsedRef) {
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
severity: "error",
|
|
302
|
+
message: `Invalid changes reference: ${changeRef[1] ?? ""}`,
|
|
303
|
+
line: lineNumber,
|
|
304
|
+
code: "invalid_changes_reference"
|
|
305
|
+
});
|
|
306
|
+
currentBlock = void 0;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
currentBlock = {
|
|
310
|
+
id: `${currentGroup.id}-block-${currentGroup.blocks.length + 1}`,
|
|
311
|
+
path: parsedRef.path,
|
|
312
|
+
kind: "change",
|
|
313
|
+
...parsedRef.oldRange === void 0 ? {} : { oldRange: parsedRef.oldRange },
|
|
314
|
+
...parsedRef.newRange === void 0 ? {} : { newRange: parsedRef.newRange },
|
|
315
|
+
notes: [],
|
|
316
|
+
line: lineNumber,
|
|
317
|
+
rawReference: changeRef[1] ?? ""
|
|
318
|
+
};
|
|
319
|
+
currentGroup.blocks.push(currentBlock);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const contextRef = CONTEXT_REF.exec(rawLine.trim());
|
|
323
|
+
if (contextRef) {
|
|
324
|
+
const parsedRef = parseContextReference(contextRef[1] ?? "");
|
|
325
|
+
if (!parsedRef) {
|
|
326
|
+
diagnostics.push({
|
|
327
|
+
severity: "error",
|
|
328
|
+
message: `Invalid context reference: ${contextRef[1] ?? ""}`,
|
|
329
|
+
line: lineNumber,
|
|
330
|
+
code: "invalid_context_reference"
|
|
331
|
+
});
|
|
332
|
+
currentBlock = void 0;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
currentBlock = {
|
|
336
|
+
id: `${currentGroup.id}-block-${currentGroup.blocks.length + 1}`,
|
|
337
|
+
path: parsedRef.path,
|
|
338
|
+
kind: "context",
|
|
339
|
+
newRange: parsedRef.range,
|
|
340
|
+
notes: [],
|
|
341
|
+
line: lineNumber,
|
|
342
|
+
rawReference: contextRef[1] ?? ""
|
|
343
|
+
};
|
|
344
|
+
currentGroup.blocks.push(currentBlock);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const noteRef = NOTE_REF.exec(rawLine);
|
|
348
|
+
if (noteRef) {
|
|
349
|
+
const side = noteRef[2]?.toLowerCase();
|
|
350
|
+
if (!currentBlock) {
|
|
351
|
+
diagnostics.push({
|
|
352
|
+
severity: "error",
|
|
353
|
+
message: `${side ?? "line"} reference has no preceding changes:// block.`,
|
|
354
|
+
line: lineNumber,
|
|
355
|
+
code: "unbound_line_reference"
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const range = parseLineRange(noteRef[3] ?? "");
|
|
360
|
+
if (!range) {
|
|
361
|
+
diagnostics.push({
|
|
362
|
+
severity: "error",
|
|
363
|
+
message: `Invalid ${side} line range: ${noteRef[3] ?? ""}`,
|
|
364
|
+
line: lineNumber,
|
|
365
|
+
code: "invalid_line_range"
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
currentBlock.notes.push({
|
|
370
|
+
id: `${currentBlock.id}-note-${currentBlock.notes.length + 1}`,
|
|
371
|
+
side,
|
|
372
|
+
severity: normalizeNoteSeverity(noteRef[1]),
|
|
373
|
+
range,
|
|
374
|
+
body: noteRef[4] ?? "",
|
|
375
|
+
path: currentBlock.path,
|
|
376
|
+
blockId: currentBlock.id,
|
|
377
|
+
line: lineNumber
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
currentGroup.bodyLines.push(rawLine);
|
|
382
|
+
}
|
|
383
|
+
const trimmedOverview = trimOuterBlankLines(overviewLines);
|
|
384
|
+
let title;
|
|
385
|
+
if (trimmedOverview.length > 0) {
|
|
386
|
+
const firstLine = trimmedOverview[0]?.trim() ?? "";
|
|
387
|
+
const titleMatch = /^#\s+(.+)$/u.exec(firstLine);
|
|
388
|
+
if (titleMatch) {
|
|
389
|
+
title = titleMatch[1]?.trim();
|
|
390
|
+
trimmedOverview.shift();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const overview = trimOuterBlankLines(trimmedOverview).join("\n").trim();
|
|
394
|
+
const resolvedGroups = groups.map((group) => ({
|
|
395
|
+
id: group.id,
|
|
396
|
+
title: group.title,
|
|
397
|
+
...group.changedLines === void 0 ? {} : { changedLines: group.changedLines },
|
|
398
|
+
commits: group.commits,
|
|
399
|
+
bodyMarkdown: trimOuterBlankLines(group.bodyLines).join("\n"),
|
|
400
|
+
blocks: group.blocks.map((block) => ({
|
|
401
|
+
id: block.id,
|
|
402
|
+
path: block.path,
|
|
403
|
+
kind: block.kind,
|
|
404
|
+
...block.oldRange === void 0 ? {} : { oldRange: block.oldRange },
|
|
405
|
+
...block.newRange === void 0 ? {} : { newRange: block.newRange },
|
|
406
|
+
notes: block.notes,
|
|
407
|
+
line: block.line,
|
|
408
|
+
rawReference: block.rawReference
|
|
409
|
+
})),
|
|
410
|
+
line: group.line
|
|
411
|
+
}));
|
|
412
|
+
const findings = parseReviewFindings(reviewLines, reviewStartLine);
|
|
413
|
+
return {
|
|
414
|
+
groups: resolvedGroups,
|
|
415
|
+
findings,
|
|
416
|
+
...title === void 0 ? {} : { title },
|
|
417
|
+
...overview.length === 0 ? {} : { overview }
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function parseCommits(value) {
|
|
421
|
+
const commits = [];
|
|
422
|
+
for (const match of value.matchAll(CODE_SPAN)) {
|
|
423
|
+
const commit = match[1]?.trim();
|
|
424
|
+
if (commit) {
|
|
425
|
+
commits.push(commit);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (commits.length > 0) {
|
|
429
|
+
return commits;
|
|
430
|
+
}
|
|
431
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
432
|
+
}
|
|
433
|
+
function parseChangeReference(reference) {
|
|
434
|
+
if (!reference.startsWith("changes://")) {
|
|
435
|
+
return void 0;
|
|
436
|
+
}
|
|
437
|
+
const withoutScheme = reference.slice("changes://".length);
|
|
438
|
+
const queryStart = withoutScheme.indexOf("?");
|
|
439
|
+
const rawPath = queryStart === -1 ? withoutScheme : withoutScheme.slice(0, queryStart);
|
|
440
|
+
const query = queryStart === -1 ? "" : withoutScheme.slice(queryStart + 1);
|
|
441
|
+
const path = safeDecode(rawPath);
|
|
442
|
+
if (path.length === 0 || path.startsWith("/")) {
|
|
443
|
+
return void 0;
|
|
444
|
+
}
|
|
445
|
+
const params = new URLSearchParams(query);
|
|
446
|
+
const oldRange = parseLineRange(params.get("old") ?? "");
|
|
447
|
+
const newRange = parseLineRange(params.get("new") ?? "");
|
|
448
|
+
return {
|
|
449
|
+
path,
|
|
450
|
+
...oldRange === void 0 ? {} : { oldRange },
|
|
451
|
+
...newRange === void 0 ? {} : { newRange }
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function parseContextReference(reference) {
|
|
455
|
+
if (!reference.startsWith("context://")) {
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
const withoutScheme = reference.slice("context://".length);
|
|
459
|
+
const queryStart = withoutScheme.indexOf("?");
|
|
460
|
+
const rawPath = queryStart === -1 ? withoutScheme : withoutScheme.slice(0, queryStart);
|
|
461
|
+
const query = queryStart === -1 ? "" : withoutScheme.slice(queryStart + 1);
|
|
462
|
+
const path = safeDecode(rawPath);
|
|
463
|
+
if (path.length === 0 || path.startsWith("/")) {
|
|
464
|
+
return void 0;
|
|
465
|
+
}
|
|
466
|
+
const params = new URLSearchParams(query);
|
|
467
|
+
const range = parseLineRange(params.get("range") ?? params.get("L") ?? "");
|
|
468
|
+
if (range === void 0) {
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
return { path, range };
|
|
472
|
+
}
|
|
473
|
+
function parseLineRange(value) {
|
|
474
|
+
const trimmed = value.trim();
|
|
475
|
+
const match = /^L(\d+)(?:-L?(\d+))?$/iu.exec(trimmed);
|
|
476
|
+
if (!match) {
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
const start = Number(match[1]);
|
|
480
|
+
const end = Number(match[2] ?? match[1]);
|
|
481
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start <= 0 || end < start) {
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
return { start, end };
|
|
485
|
+
}
|
|
486
|
+
function trimOuterBlankLines(lines) {
|
|
487
|
+
let start = 0;
|
|
488
|
+
let end = lines.length;
|
|
489
|
+
while (start < end && lines[start]?.trim() === "") {
|
|
490
|
+
start += 1;
|
|
491
|
+
}
|
|
492
|
+
while (end > start && lines[end - 1]?.trim() === "") {
|
|
493
|
+
end -= 1;
|
|
494
|
+
}
|
|
495
|
+
return lines.slice(start, end);
|
|
496
|
+
}
|
|
497
|
+
function safeDecode(value) {
|
|
498
|
+
try {
|
|
499
|
+
return decodeURIComponent(value);
|
|
500
|
+
} catch {
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function isValidUrl(value) {
|
|
505
|
+
try {
|
|
506
|
+
void new URL(value);
|
|
507
|
+
return true;
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function validateParsedReviewDocument(document) {
|
|
513
|
+
const diagnostics = [...document.diagnostics];
|
|
514
|
+
const budget = document.frontmatter.lineBudget;
|
|
515
|
+
for (const group of document.groups) {
|
|
516
|
+
if (group.changedLines !== void 0 && group.changedLines > budget) {
|
|
517
|
+
diagnostics.push({
|
|
518
|
+
severity: "warning",
|
|
519
|
+
message: `Group "${group.title}" declares ${group.changedLines} changed lines, above budget ${budget}.`,
|
|
520
|
+
line: group.line,
|
|
521
|
+
code: "group_over_budget"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const changeBlocks = group.blocks.filter((block) => block.kind === "change");
|
|
525
|
+
if (group.blocks.length === 0) {
|
|
526
|
+
diagnostics.push({
|
|
527
|
+
severity: "warning",
|
|
528
|
+
message: `Group "${group.title}" has no changes:// or context:// block.`,
|
|
529
|
+
line: group.line,
|
|
530
|
+
code: "group_without_blocks"
|
|
531
|
+
});
|
|
532
|
+
} else if (changeBlocks.length === 0) {
|
|
533
|
+
diagnostics.push({
|
|
534
|
+
severity: "info",
|
|
535
|
+
message: `Group "${group.title}" contains only context:// blocks.`,
|
|
536
|
+
line: group.line,
|
|
537
|
+
code: "group_only_context_blocks"
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const renderedPaths = /* @__PURE__ */ new Set();
|
|
542
|
+
for (const group of document.groups) {
|
|
543
|
+
for (const block of group.blocks) {
|
|
544
|
+
renderedPaths.add(block.path);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const finding of document.findings ?? []) {
|
|
548
|
+
for (const ref of finding.refs) {
|
|
549
|
+
if (!renderedPaths.has(ref.path)) {
|
|
550
|
+
diagnostics.push({
|
|
551
|
+
severity: "warning",
|
|
552
|
+
message: `Review finding references "${ref.path}", which is not shown in any group (its chip will not jump anywhere).`,
|
|
553
|
+
line: finding.line,
|
|
554
|
+
code: "finding_ref_unresolved"
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return diagnostics;
|
|
560
|
+
}
|
|
561
|
+
function collectBundleDiagnostics(bundle) {
|
|
562
|
+
return [
|
|
563
|
+
...bundle.diagnostics,
|
|
564
|
+
...bundle.groups.flatMap((group) => group.diagnostics),
|
|
565
|
+
...bundle.groups.flatMap((group) => group.blocks.flatMap((block) => block.diagnostics)),
|
|
566
|
+
...Object.values(bundle.files).flatMap((file) => file.diagnostics)
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
function validateResolvedBlock(block) {
|
|
570
|
+
const diagnostics = [];
|
|
571
|
+
const file = block.file;
|
|
572
|
+
if (!file) {
|
|
573
|
+
diagnostics.push({
|
|
574
|
+
severity: "error",
|
|
575
|
+
message: `No Git diff data was resolved for ${block.path}.`,
|
|
576
|
+
line: block.line,
|
|
577
|
+
code: "missing_resolved_file"
|
|
578
|
+
});
|
|
579
|
+
return diagnostics;
|
|
580
|
+
}
|
|
581
|
+
validateRange(block.oldRange, file.oldText, "old", block.line, diagnostics);
|
|
582
|
+
validateRange(block.newRange, file.newText, "new", block.line, diagnostics);
|
|
583
|
+
for (const note of block.notes) {
|
|
584
|
+
validateRange(
|
|
585
|
+
note.range,
|
|
586
|
+
note.side === "old" ? file.oldText : file.newText,
|
|
587
|
+
note.side,
|
|
588
|
+
note.line,
|
|
589
|
+
diagnostics
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return diagnostics;
|
|
593
|
+
}
|
|
594
|
+
function validateRange(range, text, side, line, diagnostics) {
|
|
595
|
+
if (!range) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const lineCount = countLines(text);
|
|
599
|
+
if (range.end > lineCount) {
|
|
600
|
+
diagnostics.push({
|
|
601
|
+
severity: "error",
|
|
602
|
+
message: `${side}://L${range.start}-L${range.end} exceeds ${side} file line count ${lineCount}.`,
|
|
603
|
+
line,
|
|
604
|
+
code: "line_reference_out_of_bounds"
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function countLines(text) {
|
|
609
|
+
if (text.length === 0) {
|
|
610
|
+
return 0;
|
|
611
|
+
}
|
|
612
|
+
return text.endsWith("\n") ? text.slice(0, -1).split("\n").length : text.split("\n").length;
|
|
613
|
+
}
|
|
614
|
+
function hasErrorDiagnostics(diagnostics) {
|
|
615
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
616
|
+
}
|
|
617
|
+
const RANGE_CONTEXT_LINES = 3;
|
|
618
|
+
function createSparseTextForRanges(text, ranges, contextLines = RANGE_CONTEXT_LINES) {
|
|
619
|
+
if (ranges.length === 0) {
|
|
620
|
+
return text;
|
|
621
|
+
}
|
|
622
|
+
const lines = splitSourceLines(text);
|
|
623
|
+
const keep = /* @__PURE__ */ new Set();
|
|
624
|
+
for (const range of ranges) {
|
|
625
|
+
const start = Math.max(1, range.start - contextLines);
|
|
626
|
+
const end = Math.min(lines.length, range.end + contextLines);
|
|
627
|
+
for (let line = start; line <= end; line += 1) {
|
|
628
|
+
keep.add(line);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return lines.map((line, index) => keep.has(index + 1) ? line : "").join("\n");
|
|
632
|
+
}
|
|
633
|
+
function splitSourceLines(text) {
|
|
634
|
+
if (text.length === 0) {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
return text.endsWith("\n") ? text.slice(0, -1).split("\n") : text.split("\n");
|
|
638
|
+
}
|
|
639
|
+
export {
|
|
640
|
+
countLines as a,
|
|
641
|
+
createSparseTextForRanges as b,
|
|
642
|
+
collectBundleDiagnostics as c,
|
|
643
|
+
parseReviewMarkdown as d,
|
|
644
|
+
parseReviewRef as e,
|
|
645
|
+
validateResolvedBlock as f,
|
|
646
|
+
hasErrorDiagnostics as h,
|
|
647
|
+
parseLineRange as p,
|
|
648
|
+
validateParsedReviewDocument as v
|
|
649
|
+
};
|