@demon-utils/playwright 0.1.5 → 0.1.6
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/bin/demon-demo-review.js +549 -67
- package/dist/bin/demon-demo-review.js.map +7 -6
- package/dist/index.js +911 -63
- package/dist/index.js.map +8 -5
- package/package.json +1 -1
- package/src/bin/demon-demo-review.ts +74 -9
- package/src/git-context.test.ts +90 -0
- package/src/git-context.ts +62 -0
- package/src/html-generator.e2e.test.ts +349 -0
- package/src/html-generator.test.ts +380 -14
- package/src/html-generator.ts +342 -33
- package/src/index.ts +9 -3
- package/src/recorder.test.ts +161 -0
- package/src/recorder.ts +74 -0
- package/src/review-types.ts +19 -1
- package/src/review.test.ts +257 -59
- package/src/review.ts +147 -31
package/src/review.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ReviewMetadata } from "./review-types.ts";
|
|
2
|
-
|
|
3
1
|
export type SpawnFn = (
|
|
4
2
|
cmd: string[],
|
|
5
3
|
) => { exitCode: Promise<number>; stdout: ReadableStream<Uint8Array> };
|
|
@@ -9,33 +7,84 @@ export interface InvokeClaudeOptions {
|
|
|
9
7
|
spawn?: SpawnFn;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const GIT_DIFF_MAX_CHARS = 50_000;
|
|
11
|
+
|
|
12
|
+
export interface BuildReviewPromptOptions {
|
|
13
|
+
filenames: string[];
|
|
14
|
+
stepsMap: Record<string, Array<{ text: string; timestampSeconds: number }>>;
|
|
15
|
+
gitDiff?: string;
|
|
16
|
+
guidelines?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildReviewPrompt(options: BuildReviewPromptOptions): string {
|
|
20
|
+
const { filenames, stepsMap, gitDiff, guidelines } = options;
|
|
21
|
+
|
|
22
|
+
const demoEntries = filenames.map((f) => {
|
|
23
|
+
const steps = stepsMap[f] ?? [];
|
|
24
|
+
const stepLines = steps
|
|
25
|
+
.map((s) => `- [${s.timestampSeconds}s] ${s.text}`)
|
|
26
|
+
.join("\n");
|
|
27
|
+
return `Video: ${f}\nRecorded steps:\n${stepLines || "(no steps recorded)"}`;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const sections: string[] = [];
|
|
31
|
+
|
|
32
|
+
if (guidelines && guidelines.length > 0) {
|
|
33
|
+
sections.push(`## Coding Guidelines\n\n${guidelines.join("\n\n")}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (gitDiff) {
|
|
37
|
+
let diff = gitDiff;
|
|
38
|
+
if (diff.length > GIT_DIFF_MAX_CHARS) {
|
|
39
|
+
diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + "\n\n... (diff truncated at 50k characters)";
|
|
40
|
+
}
|
|
41
|
+
sections.push(`## Git Diff\n\n\`\`\`diff\n${diff}\n\`\`\``);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sections.push(`## Demo Recordings\n\n${demoEntries.join("\n\n")}`);
|
|
14
45
|
|
|
15
|
-
return `You are given
|
|
46
|
+
return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.
|
|
16
47
|
|
|
17
|
-
${
|
|
48
|
+
${sections.join("\n\n")}
|
|
18
49
|
|
|
19
|
-
|
|
50
|
+
## Task
|
|
51
|
+
|
|
52
|
+
Review the code changes and demo recordings. Generate a JSON object matching this exact schema:
|
|
20
53
|
|
|
21
54
|
{
|
|
22
55
|
"demos": [
|
|
23
56
|
{
|
|
24
57
|
"file": "<filename>",
|
|
25
|
-
"summary": "<a
|
|
26
|
-
"annotations": [
|
|
27
|
-
{ "timestampSeconds": <number>, "text": "<annotation text>" }
|
|
28
|
-
]
|
|
58
|
+
"summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
|
|
29
59
|
}
|
|
30
|
-
]
|
|
60
|
+
],
|
|
61
|
+
"review": {
|
|
62
|
+
"summary": "<2-3 sentence overview of the changes>",
|
|
63
|
+
"highlights": ["<positive aspect 1>", "<positive aspect 2>"],
|
|
64
|
+
"verdict": "approve" | "request_changes",
|
|
65
|
+
"verdictReason": "<one sentence justifying the verdict>",
|
|
66
|
+
"issues": [
|
|
67
|
+
{
|
|
68
|
+
"severity": "major" | "minor" | "nit",
|
|
69
|
+
"description": "<what the issue is and how to fix it>"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
31
73
|
}
|
|
32
74
|
|
|
33
75
|
Rules:
|
|
34
76
|
- Return ONLY the JSON object, no markdown fences or extra text.
|
|
35
77
|
- Include one entry in "demos" for each filename, in the same order.
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- "
|
|
78
|
+
- "file" must exactly match the provided filename.
|
|
79
|
+
- "verdict" must be exactly "approve" or "request_changes".
|
|
80
|
+
- Use "request_changes" if there are any "major" issues.
|
|
81
|
+
- "severity" must be exactly "major", "minor", or "nit".
|
|
82
|
+
- "major": bugs, security issues, broken functionality, guideline violations.
|
|
83
|
+
- "minor": code quality, readability, missing edge cases.
|
|
84
|
+
- "nit": style, naming, trivial improvements.
|
|
85
|
+
- "highlights" must have at least one entry.
|
|
86
|
+
- "issues" can be an empty array if there are no issues.
|
|
87
|
+
- Verify that demo steps demonstrate the acceptance criteria being met.`;
|
|
39
88
|
}
|
|
40
89
|
|
|
41
90
|
export async function invokeClaude(
|
|
@@ -68,10 +117,46 @@ export async function invokeClaude(
|
|
|
68
117
|
return output.trim();
|
|
69
118
|
}
|
|
70
119
|
|
|
71
|
-
|
|
120
|
+
import type { IssueSeverity, ReviewVerdict } from "./review-types.ts";
|
|
121
|
+
|
|
122
|
+
export interface LlmReviewResponse {
|
|
123
|
+
demos: Array<{ file: string; summary: string }>;
|
|
124
|
+
review: {
|
|
125
|
+
summary: string;
|
|
126
|
+
highlights: string[];
|
|
127
|
+
verdict: ReviewVerdict;
|
|
128
|
+
verdictReason: string;
|
|
129
|
+
issues: Array<{ severity: IssueSeverity; description: string }>;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const VALID_VERDICTS: ReadonlySet<string> = new Set(["approve", "request_changes"]);
|
|
134
|
+
const VALID_SEVERITIES: ReadonlySet<string> = new Set(["major", "minor", "nit"]);
|
|
135
|
+
|
|
136
|
+
export function extractJson(raw: string): string {
|
|
137
|
+
// Try raw string first
|
|
138
|
+
try {
|
|
139
|
+
JSON.parse(raw);
|
|
140
|
+
return raw;
|
|
141
|
+
} catch {
|
|
142
|
+
// look for first { and last }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const start = raw.indexOf("{");
|
|
146
|
+
const end = raw.lastIndexOf("}");
|
|
147
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
148
|
+
throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return raw.slice(start, end + 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function parseLlmResponse(raw: string): LlmReviewResponse {
|
|
155
|
+
const jsonStr = extractJson(raw);
|
|
156
|
+
|
|
72
157
|
let parsed: unknown;
|
|
73
158
|
try {
|
|
74
|
-
parsed = JSON.parse(
|
|
159
|
+
parsed = JSON.parse(jsonStr);
|
|
75
160
|
} catch {
|
|
76
161
|
throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
|
|
77
162
|
}
|
|
@@ -97,25 +182,56 @@ export function parseReviewMetadata(raw: string): ReviewMetadata {
|
|
|
97
182
|
if (typeof d["summary"] !== "string") {
|
|
98
183
|
throw new Error("Each demo must have a 'summary' string");
|
|
99
184
|
}
|
|
100
|
-
|
|
101
|
-
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (typeof obj["review"] !== "object" || obj["review"] === null) {
|
|
188
|
+
throw new Error("Missing 'review' object in response");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const review = obj["review"] as Record<string, unknown>;
|
|
192
|
+
|
|
193
|
+
if (typeof review["summary"] !== "string") {
|
|
194
|
+
throw new Error("review.summary must be a string");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!Array.isArray(review["highlights"])) {
|
|
198
|
+
throw new Error("review.highlights must be an array");
|
|
199
|
+
}
|
|
200
|
+
if (review["highlights"].length === 0) {
|
|
201
|
+
throw new Error("review.highlights must not be empty");
|
|
202
|
+
}
|
|
203
|
+
for (const h of review["highlights"]) {
|
|
204
|
+
if (typeof h !== "string") {
|
|
205
|
+
throw new Error("Each highlight must be a string");
|
|
102
206
|
}
|
|
207
|
+
}
|
|
103
208
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
209
|
+
if (typeof review["verdict"] !== "string" || !VALID_VERDICTS.has(review["verdict"])) {
|
|
210
|
+
throw new Error("review.verdict must be 'approve' or 'request_changes'");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof review["verdictReason"] !== "string") {
|
|
214
|
+
throw new Error("review.verdictReason must be a string");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!Array.isArray(review["issues"])) {
|
|
218
|
+
throw new Error("review.issues must be an array");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const issue of review["issues"] as unknown[]) {
|
|
222
|
+
if (typeof issue !== "object" || issue === null) {
|
|
223
|
+
throw new Error("Each issue must be an object");
|
|
224
|
+
}
|
|
225
|
+
const i = issue as Record<string, unknown>;
|
|
226
|
+
if (typeof i["severity"] !== "string" || !VALID_SEVERITIES.has(i["severity"])) {
|
|
227
|
+
throw new Error("Each issue severity must be 'major', 'minor', or 'nit'");
|
|
228
|
+
}
|
|
229
|
+
if (typeof i["description"] !== "string") {
|
|
230
|
+
throw new Error("Each issue must have a 'description' string");
|
|
115
231
|
}
|
|
116
232
|
}
|
|
117
233
|
|
|
118
|
-
return parsed as
|
|
234
|
+
return parsed as LlmReviewResponse;
|
|
119
235
|
}
|
|
120
236
|
|
|
121
237
|
function defaultSpawn(
|