@demon-utils/playwright 0.1.6 → 0.1.7
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-init.js +56 -0
- package/dist/bin/demon-demo-init.js.map +10 -0
- package/dist/bin/demon-demo-review.js +187 -523
- package/dist/bin/demon-demo-review.js.map +7 -7
- package/dist/bin/demoon.js +1445 -0
- package/dist/bin/demoon.js.map +22 -0
- package/dist/bin/review-template.html +62 -0
- package/dist/github-issue.js +749 -0
- package/dist/github-issue.js.map +16 -0
- package/dist/index.js +1320 -867
- package/dist/index.js.map +16 -8
- package/dist/orchestrator.js +1421 -0
- package/dist/orchestrator.js.map +20 -0
- package/dist/review-generator.js +424 -0
- package/dist/review-generator.js.map +12 -0
- package/dist/review-template.html +62 -0
- package/package.json +11 -2
- package/src/bin/demon-demo-init.ts +59 -0
- package/src/bin/demon-demo-review.ts +19 -97
- package/src/bin/demoon.ts +140 -0
- package/src/feedback-server.ts +138 -0
- package/src/git-context.test.ts +68 -2
- package/src/git-context.ts +48 -9
- package/src/github-issue.test.ts +188 -0
- package/src/github-issue.ts +139 -0
- package/src/html-generator.e2e.test.ts +361 -80
- package/src/index.ts +9 -3
- package/src/orchestrator.test.ts +183 -0
- package/src/orchestrator.ts +341 -0
- package/src/review-generator.ts +221 -0
- package/src/review-types.ts +3 -0
- package/src/review.ts +13 -7
- package/src/html-generator.test.ts +0 -561
- package/src/html-generator.ts +0 -461
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// src/review-generator.ts
|
|
21
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
22
|
+
import { join, dirname, relative } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
// src/review.ts
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
import { Readable } from "node:stream";
|
|
28
|
+
var GIT_DIFF_MAX_CHARS = 50000;
|
|
29
|
+
function buildReviewPrompt(options) {
|
|
30
|
+
const { filenames, stepsMap, gitDiff, guidelines } = options;
|
|
31
|
+
const demoEntries = filenames.map((f) => {
|
|
32
|
+
const steps = stepsMap[f] ?? [];
|
|
33
|
+
const stepLines = steps.map((s) => `- [${s.timestampSeconds}s] ${s.text}`).join(`
|
|
34
|
+
`);
|
|
35
|
+
return `Video: ${f}
|
|
36
|
+
Recorded steps:
|
|
37
|
+
${stepLines || "(no steps recorded)"}`;
|
|
38
|
+
});
|
|
39
|
+
const sections = [];
|
|
40
|
+
if (guidelines && guidelines.length > 0) {
|
|
41
|
+
sections.push(`## Coding Guidelines
|
|
42
|
+
|
|
43
|
+
${guidelines.join(`
|
|
44
|
+
|
|
45
|
+
`)}`);
|
|
46
|
+
}
|
|
47
|
+
if (gitDiff) {
|
|
48
|
+
let diff = gitDiff;
|
|
49
|
+
if (diff.length > GIT_DIFF_MAX_CHARS) {
|
|
50
|
+
diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + `
|
|
51
|
+
|
|
52
|
+
... (diff truncated at 50k characters)`;
|
|
53
|
+
}
|
|
54
|
+
sections.push(`## Git Diff
|
|
55
|
+
|
|
56
|
+
\`\`\`diff
|
|
57
|
+
${diff}
|
|
58
|
+
\`\`\``);
|
|
59
|
+
}
|
|
60
|
+
sections.push(`## Demo Recordings
|
|
61
|
+
|
|
62
|
+
${demoEntries.join(`
|
|
63
|
+
|
|
64
|
+
`)}`);
|
|
65
|
+
return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.
|
|
66
|
+
|
|
67
|
+
${sections.join(`
|
|
68
|
+
|
|
69
|
+
`)}
|
|
70
|
+
|
|
71
|
+
## Task
|
|
72
|
+
|
|
73
|
+
Review the code changes and demo recordings. Generate a JSON object matching this exact schema:
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
"demos": [
|
|
77
|
+
{
|
|
78
|
+
"file": "<filename>",
|
|
79
|
+
"summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"review": {
|
|
83
|
+
"summary": "<2-3 sentence overview of the changes>",
|
|
84
|
+
"highlights": ["<positive aspect 1>", "<positive aspect 2>"],
|
|
85
|
+
"verdict": "approve" | "request_changes",
|
|
86
|
+
"verdictReason": "<one sentence justifying the verdict>",
|
|
87
|
+
"issues": [
|
|
88
|
+
{
|
|
89
|
+
"severity": "major" | "minor" | "nit",
|
|
90
|
+
"description": "<what the issue is and how to fix it>"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Rules:
|
|
97
|
+
- Return ONLY the JSON object, no markdown fences or extra text.
|
|
98
|
+
- Include one entry in "demos" for each filename, in the same order.
|
|
99
|
+
- "file" must exactly match the provided filename.
|
|
100
|
+
- "verdict" must be exactly "approve" or "request_changes".
|
|
101
|
+
- Use "request_changes" if there are any "major" issues.
|
|
102
|
+
- "severity" must be exactly "major", "minor", or "nit".
|
|
103
|
+
- "major": bugs, security issues, broken functionality, guideline violations.
|
|
104
|
+
- "minor": code quality, readability, missing edge cases.
|
|
105
|
+
- "nit": style, naming, trivial improvements.
|
|
106
|
+
- "highlights" must have at least one entry.
|
|
107
|
+
- "issues" can be an empty array if there are no issues.
|
|
108
|
+
- Verify that demo steps demonstrate the acceptance criteria being met.`;
|
|
109
|
+
}
|
|
110
|
+
async function invokeClaude(prompt, options) {
|
|
111
|
+
const spawnFn = options?.spawn ?? defaultSpawn;
|
|
112
|
+
const agent = options?.agent ?? "claude";
|
|
113
|
+
const proc = spawnFn([agent, "-p", prompt]);
|
|
114
|
+
const reader = proc.stdout.getReader();
|
|
115
|
+
const chunks = [];
|
|
116
|
+
for (;; ) {
|
|
117
|
+
const { done, value } = await reader.read();
|
|
118
|
+
if (done)
|
|
119
|
+
break;
|
|
120
|
+
chunks.push(value);
|
|
121
|
+
}
|
|
122
|
+
const exitCode = await proc.exitCode;
|
|
123
|
+
const output = new TextDecoder().decode(concatUint8Arrays(chunks));
|
|
124
|
+
if (exitCode !== 0) {
|
|
125
|
+
throw new Error(`claude process exited with code ${exitCode}: ${output.trim()}`);
|
|
126
|
+
}
|
|
127
|
+
return output.trim();
|
|
128
|
+
}
|
|
129
|
+
var VALID_VERDICTS = new Set(["approve", "request_changes"]);
|
|
130
|
+
var VALID_SEVERITIES = new Set(["major", "minor", "nit"]);
|
|
131
|
+
function extractJson(raw) {
|
|
132
|
+
try {
|
|
133
|
+
JSON.parse(raw);
|
|
134
|
+
return raw;
|
|
135
|
+
} catch {}
|
|
136
|
+
const start = raw.indexOf("{");
|
|
137
|
+
const end = raw.lastIndexOf("}");
|
|
138
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
139
|
+
throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);
|
|
140
|
+
}
|
|
141
|
+
return raw.slice(start, end + 1);
|
|
142
|
+
}
|
|
143
|
+
function parseLlmResponse(raw) {
|
|
144
|
+
const jsonStr = extractJson(raw);
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(jsonStr);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
|
|
150
|
+
}
|
|
151
|
+
if (typeof parsed !== "object" || parsed === null || !("demos" in parsed)) {
|
|
152
|
+
throw new Error("Missing 'demos' array in review metadata");
|
|
153
|
+
}
|
|
154
|
+
const obj = parsed;
|
|
155
|
+
if (!Array.isArray(obj["demos"])) {
|
|
156
|
+
throw new Error("'demos' must be an array");
|
|
157
|
+
}
|
|
158
|
+
for (const demo of obj["demos"]) {
|
|
159
|
+
if (typeof demo !== "object" || demo === null) {
|
|
160
|
+
throw new Error("Each demo must be an object");
|
|
161
|
+
}
|
|
162
|
+
const d = demo;
|
|
163
|
+
if (typeof d["file"] !== "string") {
|
|
164
|
+
throw new Error("Each demo must have a 'file' string");
|
|
165
|
+
}
|
|
166
|
+
if (typeof d["summary"] !== "string") {
|
|
167
|
+
throw new Error("Each demo must have a 'summary' string");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (typeof obj["review"] !== "object" || obj["review"] === null) {
|
|
171
|
+
throw new Error("Missing 'review' object in response");
|
|
172
|
+
}
|
|
173
|
+
const review = obj["review"];
|
|
174
|
+
if (typeof review["summary"] !== "string") {
|
|
175
|
+
throw new Error("review.summary must be a string");
|
|
176
|
+
}
|
|
177
|
+
if (!Array.isArray(review["highlights"])) {
|
|
178
|
+
throw new Error("review.highlights must be an array");
|
|
179
|
+
}
|
|
180
|
+
if (review["highlights"].length === 0) {
|
|
181
|
+
throw new Error("review.highlights must not be empty");
|
|
182
|
+
}
|
|
183
|
+
for (const h of review["highlights"]) {
|
|
184
|
+
if (typeof h !== "string") {
|
|
185
|
+
throw new Error("Each highlight must be a string");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (typeof review["verdict"] !== "string" || !VALID_VERDICTS.has(review["verdict"])) {
|
|
189
|
+
throw new Error("review.verdict must be 'approve' or 'request_changes'");
|
|
190
|
+
}
|
|
191
|
+
if (typeof review["verdictReason"] !== "string") {
|
|
192
|
+
throw new Error("review.verdictReason must be a string");
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(review["issues"])) {
|
|
195
|
+
throw new Error("review.issues must be an array");
|
|
196
|
+
}
|
|
197
|
+
for (const issue of review["issues"]) {
|
|
198
|
+
if (typeof issue !== "object" || issue === null) {
|
|
199
|
+
throw new Error("Each issue must be an object");
|
|
200
|
+
}
|
|
201
|
+
const i = issue;
|
|
202
|
+
if (typeof i["severity"] !== "string" || !VALID_SEVERITIES.has(i["severity"])) {
|
|
203
|
+
throw new Error("Each issue severity must be 'major', 'minor', or 'nit'");
|
|
204
|
+
}
|
|
205
|
+
if (typeof i["description"] !== "string") {
|
|
206
|
+
throw new Error("Each issue must have a 'description' string");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return parsed;
|
|
210
|
+
}
|
|
211
|
+
function defaultSpawn(cmd) {
|
|
212
|
+
const [command, ...args] = cmd;
|
|
213
|
+
const proc = spawn(command, args, {
|
|
214
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
215
|
+
});
|
|
216
|
+
const exitCode = new Promise((resolve) => {
|
|
217
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
218
|
+
});
|
|
219
|
+
const stdout = Readable.toWeb(proc.stdout);
|
|
220
|
+
return { exitCode, stdout };
|
|
221
|
+
}
|
|
222
|
+
function concatUint8Arrays(arrays) {
|
|
223
|
+
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
224
|
+
const result = new Uint8Array(totalLength);
|
|
225
|
+
let offset = 0;
|
|
226
|
+
for (const a of arrays) {
|
|
227
|
+
result.set(a, offset);
|
|
228
|
+
offset += a.length;
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/git-context.ts
|
|
234
|
+
import { readFileSync } from "node:fs";
|
|
235
|
+
import { spawnSync } from "node:child_process";
|
|
236
|
+
async function detectDefaultBase(exec, gitRoot) {
|
|
237
|
+
let currentBranch;
|
|
238
|
+
try {
|
|
239
|
+
currentBranch = (await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"], gitRoot)).trim();
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (currentBranch === "main" || currentBranch === "master") {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
for (const candidate of ["main", "master"]) {
|
|
247
|
+
try {
|
|
248
|
+
await exec(["git", "rev-parse", "--verify", candidate], gitRoot);
|
|
249
|
+
return candidate;
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
var defaultExec = async (cmd, cwd) => {
|
|
255
|
+
const [command, ...args] = cmd;
|
|
256
|
+
const proc = spawnSync(command, args, { cwd, encoding: "utf-8" });
|
|
257
|
+
if (proc.status !== 0) {
|
|
258
|
+
const stderr = (proc.stderr ?? "").trim();
|
|
259
|
+
throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
|
|
260
|
+
}
|
|
261
|
+
return proc.stdout ?? "";
|
|
262
|
+
};
|
|
263
|
+
var defaultReadFile = (path) => {
|
|
264
|
+
return readFileSync(path, "utf-8");
|
|
265
|
+
};
|
|
266
|
+
async function getRepoContext(demosDir, options) {
|
|
267
|
+
const exec = options?.exec ?? defaultExec;
|
|
268
|
+
const readFile = options?.readFile ?? defaultReadFile;
|
|
269
|
+
const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
|
|
270
|
+
const diffBase = options?.diffBase ?? await detectDefaultBase(exec, gitRoot);
|
|
271
|
+
let gitDiff;
|
|
272
|
+
if (diffBase) {
|
|
273
|
+
gitDiff = (await exec(["git", "diff", `${diffBase}...HEAD`], gitRoot)).trim();
|
|
274
|
+
} else {
|
|
275
|
+
const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
|
|
276
|
+
if (workingDiff.length > 0) {
|
|
277
|
+
gitDiff = workingDiff;
|
|
278
|
+
} else {
|
|
279
|
+
gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
|
|
283
|
+
const files = lsOutput.split(`
|
|
284
|
+
`).filter((f) => f.length > 0);
|
|
285
|
+
const guidelinePatterns = ["CLAUDE.md", "SKILL.md"];
|
|
286
|
+
const guidelines = [];
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
const basename = file.split("/").pop() ?? "";
|
|
289
|
+
if (guidelinePatterns.includes(basename)) {
|
|
290
|
+
const fullPath = `${gitRoot}/${file}`;
|
|
291
|
+
const content = readFile(fullPath);
|
|
292
|
+
guidelines.push(`# ${file}
|
|
293
|
+
${content}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { gitDiff, guidelines };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/review-generator.ts
|
|
300
|
+
function getReviewTemplate() {
|
|
301
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
302
|
+
const distDir = dirname(currentFile);
|
|
303
|
+
const templatePath = join(distDir, "review-template.html");
|
|
304
|
+
if (!existsSync(templatePath)) {
|
|
305
|
+
throw new Error(`Review template not found at ${templatePath}. ` + `Make sure to build the review-app package first.`);
|
|
306
|
+
}
|
|
307
|
+
return readFileSync2(templatePath, "utf-8");
|
|
308
|
+
}
|
|
309
|
+
function escapeHtml(s) {
|
|
310
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
311
|
+
}
|
|
312
|
+
function generateReviewHtml(appData) {
|
|
313
|
+
const template = getReviewTemplate();
|
|
314
|
+
const jsonData = JSON.stringify(appData);
|
|
315
|
+
return template.replace("<title>Demo Review</title>", `<title>${escapeHtml(appData.title)}</title>`).replace('"{{__INJECT_REVIEW_DATA__}}"', jsonData);
|
|
316
|
+
}
|
|
317
|
+
function discoverDemoFiles(directory) {
|
|
318
|
+
const files = [];
|
|
319
|
+
const processFile = (filePath, filename) => {
|
|
320
|
+
const relativePath = relative(directory, filePath);
|
|
321
|
+
if (filename.endsWith(".webm")) {
|
|
322
|
+
files.push({ path: filePath, filename, relativePath, type: "web-ux" });
|
|
323
|
+
} else if (filename.endsWith(".jsonl")) {
|
|
324
|
+
files.push({ path: filePath, filename, relativePath, type: "log-based" });
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
for (const f of readdirSync(directory)) {
|
|
328
|
+
processFile(join(directory, f), f);
|
|
329
|
+
}
|
|
330
|
+
if (files.length === 0) {
|
|
331
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
332
|
+
if (!entry.isDirectory())
|
|
333
|
+
continue;
|
|
334
|
+
const subdir = join(directory, entry.name);
|
|
335
|
+
for (const f of readdirSync(subdir)) {
|
|
336
|
+
processFile(join(subdir, f), f);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return files.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
341
|
+
}
|
|
342
|
+
async function generateReview(options) {
|
|
343
|
+
const { directory, agent, feedbackEndpoint, title = "Demo Review", diffBase } = options;
|
|
344
|
+
const demoFiles = discoverDemoFiles(directory);
|
|
345
|
+
const webUxDemos = demoFiles.filter((d) => d.type === "web-ux");
|
|
346
|
+
const logBasedDemos = demoFiles.filter((d) => d.type === "log-based");
|
|
347
|
+
if (demoFiles.length === 0) {
|
|
348
|
+
throw new Error(`No .webm or .jsonl files found in "${directory}" or its subdirectories.`);
|
|
349
|
+
}
|
|
350
|
+
const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));
|
|
351
|
+
const stepsMapByFilename = {};
|
|
352
|
+
const stepsMapByRelativePath = {};
|
|
353
|
+
for (const demo of webUxDemos) {
|
|
354
|
+
const stepsPath = join(dirname(demo.path), "demo-steps.json");
|
|
355
|
+
if (!existsSync(stepsPath))
|
|
356
|
+
continue;
|
|
357
|
+
try {
|
|
358
|
+
const raw = readFileSync2(stepsPath, "utf-8");
|
|
359
|
+
const parsed = JSON.parse(raw);
|
|
360
|
+
if (Array.isArray(parsed)) {
|
|
361
|
+
stepsMapByFilename[demo.filename] = parsed;
|
|
362
|
+
stepsMapByRelativePath[demo.relativePath] = parsed;
|
|
363
|
+
}
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
366
|
+
const logsMap = {};
|
|
367
|
+
for (const demo of logBasedDemos) {
|
|
368
|
+
logsMap[demo.relativePath] = readFileSync2(demo.path, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
const hasWebUxDemos = webUxDemos.length > 0;
|
|
371
|
+
const hasLogDemos = logBasedDemos.length > 0;
|
|
372
|
+
if (hasWebUxDemos && Object.keys(stepsMapByFilename).length === 0) {
|
|
373
|
+
throw new Error("No demo-steps.json found alongside any .webm files. " + "Use DemoRecorder in your demo tests to generate step data.");
|
|
374
|
+
}
|
|
375
|
+
if (!hasWebUxDemos && !hasLogDemos) {
|
|
376
|
+
throw new Error("No demo files found.");
|
|
377
|
+
}
|
|
378
|
+
let gitDiff;
|
|
379
|
+
let guidelines;
|
|
380
|
+
try {
|
|
381
|
+
const repoContext = await getRepoContext(directory, { diffBase });
|
|
382
|
+
gitDiff = repoContext.gitDiff;
|
|
383
|
+
guidelines = repoContext.guidelines;
|
|
384
|
+
} catch {}
|
|
385
|
+
const allFilenames = demoFiles.map((d) => d.filename);
|
|
386
|
+
const prompt = buildReviewPrompt({ filenames: allFilenames, stepsMap: stepsMapByFilename, gitDiff, guidelines });
|
|
387
|
+
const rawOutput = await invokeClaude(prompt, { agent });
|
|
388
|
+
const llmResponse = parseLlmResponse(rawOutput);
|
|
389
|
+
const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));
|
|
390
|
+
const metadata = {
|
|
391
|
+
demos: llmResponse.demos.map((demo) => {
|
|
392
|
+
const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;
|
|
393
|
+
return {
|
|
394
|
+
file: relativePath,
|
|
395
|
+
type: typeMap.get(demo.file) ?? "web-ux",
|
|
396
|
+
summary: demo.summary,
|
|
397
|
+
steps: stepsMapByRelativePath[relativePath] ?? []
|
|
398
|
+
};
|
|
399
|
+
}),
|
|
400
|
+
review: llmResponse.review
|
|
401
|
+
};
|
|
402
|
+
const metadataPath = join(directory, "review-metadata.json");
|
|
403
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + `
|
|
404
|
+
`);
|
|
405
|
+
const appData = {
|
|
406
|
+
metadata,
|
|
407
|
+
title,
|
|
408
|
+
videos: {},
|
|
409
|
+
logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,
|
|
410
|
+
feedbackEndpoint
|
|
411
|
+
};
|
|
412
|
+
const html = generateReviewHtml(appData);
|
|
413
|
+
const htmlPath = join(directory, "review.html");
|
|
414
|
+
writeFileSync(htmlPath, html);
|
|
415
|
+
return { htmlPath, metadataPath, metadata };
|
|
416
|
+
}
|
|
417
|
+
export {
|
|
418
|
+
getReviewTemplate,
|
|
419
|
+
generateReviewHtml,
|
|
420
|
+
generateReview,
|
|
421
|
+
discoverDemoFiles
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
//# debugId=D519E984A168289764756E2164756E21
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/review-generator.ts", "../src/review.ts", "../src/git-context.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { existsSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join, dirname, relative } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n buildReviewPrompt,\n invokeClaude,\n parseLlmResponse,\n} from \"./review.ts\";\nimport { getRepoContext } from \"./git-context.ts\";\nimport type { ReviewMetadata, DemoType } from \"./review-types.ts\";\n\nexport interface ReviewAppData {\n metadata: ReviewMetadata;\n title: string;\n videos: Record<string, string>;\n logs?: Record<string, string>;\n feedbackEndpoint?: string;\n}\n\nexport interface DemoFile {\n path: string;\n filename: string;\n relativePath: string;\n type: DemoType;\n}\n\nexport interface GenerateReviewOptions {\n directory: string;\n agent?: string;\n feedbackEndpoint?: string;\n title?: string;\n diffBase?: string; // Base commit/branch for diff (auto-detected if not provided)\n}\n\nexport interface GenerateReviewResult {\n htmlPath: string;\n metadataPath: string;\n metadata: ReviewMetadata;\n}\n\nexport function getReviewTemplate(): string {\n const currentFile = fileURLToPath(import.meta.url);\n const distDir = dirname(currentFile);\n const templatePath = join(distDir, \"review-template.html\");\n\n if (!existsSync(templatePath)) {\n throw new Error(\n `Review template not found at ${templatePath}. ` +\n `Make sure to build the review-app package first.`\n );\n }\n\n return readFileSync(templatePath, \"utf-8\");\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nexport function generateReviewHtml(appData: ReviewAppData): string {\n const template = getReviewTemplate();\n const jsonData = JSON.stringify(appData);\n\n return template\n .replace(\"<title>Demo Review</title>\", `<title>${escapeHtml(appData.title)}</title>`)\n .replace('\"{{__INJECT_REVIEW_DATA__}}\"', jsonData);\n}\n\n/**\n * Discover demo files (.webm and .jsonl) in the given directory.\n * Searches top-level first, then one level deep (Playwright creates per-test subdirectories).\n */\nexport function discoverDemoFiles(directory: string): DemoFile[] {\n const files: DemoFile[] = [];\n\n const processFile = (filePath: string, filename: string) => {\n const relativePath = relative(directory, filePath);\n if (filename.endsWith(\".webm\")) {\n files.push({ path: filePath, filename, relativePath, type: \"web-ux\" });\n } else if (filename.endsWith(\".jsonl\")) {\n files.push({ path: filePath, filename, relativePath, type: \"log-based\" });\n }\n };\n\n // Search top-level\n for (const f of readdirSync(directory)) {\n processFile(join(directory, f), f);\n }\n\n // If no files found at top level, search one level deep\n if (files.length === 0) {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const subdir = join(directory, entry.name);\n for (const f of readdirSync(subdir)) {\n processFile(join(subdir, f), f);\n }\n }\n }\n\n return files.sort((a, b) => a.filename.localeCompare(b.filename));\n}\n\n/**\n * Generate a review HTML page from demo files in the given directory.\n * This is the main entry point for programmatic use.\n */\nexport async function generateReview(options: GenerateReviewOptions): Promise<GenerateReviewResult> {\n const { directory, agent, feedbackEndpoint, title = \"Demo Review\", diffBase } = options;\n\n const demoFiles = discoverDemoFiles(directory);\n const webUxDemos = demoFiles.filter((d) => d.type === \"web-ux\");\n const logBasedDemos = demoFiles.filter((d) => d.type === \"log-based\");\n\n if (demoFiles.length === 0) {\n throw new Error(`No .webm or .jsonl files found in \"${directory}\" or its subdirectories.`);\n }\n\n // Build maps from filename to relativePath for lookup\n const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));\n\n // Collect demo-steps.json from the directory of each .webm file\n // Key by filename for prompt builder, and by relativePath for metadata\n const stepsMapByFilename: Record<string, Array<{ text: string; timestampSeconds: number }>> = {};\n const stepsMapByRelativePath: Record<string, Array<{ text: string; timestampSeconds: number }>> = {};\n for (const demo of webUxDemos) {\n const stepsPath = join(dirname(demo.path), \"demo-steps.json\");\n if (!existsSync(stepsPath)) continue;\n try {\n const raw = readFileSync(stepsPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) {\n stepsMapByFilename[demo.filename] = parsed;\n stepsMapByRelativePath[demo.relativePath] = parsed;\n }\n } catch {\n // skip malformed steps files\n }\n }\n\n // Collect JSONL content for log-based demos, keyed by relativePath\n const logsMap: Record<string, string> = {};\n for (const demo of logBasedDemos) {\n logsMap[demo.relativePath] = readFileSync(demo.path, \"utf-8\");\n }\n\n // For web-ux demos, require steps\n const hasWebUxDemos = webUxDemos.length > 0;\n const hasLogDemos = logBasedDemos.length > 0;\n\n if (hasWebUxDemos && Object.keys(stepsMapByFilename).length === 0) {\n throw new Error(\n \"No demo-steps.json found alongside any .webm files. \" +\n \"Use DemoRecorder in your demo tests to generate step data.\"\n );\n }\n\n if (!hasWebUxDemos && !hasLogDemos) {\n throw new Error(\"No demo files found.\");\n }\n\n // Gather repo context (git diff + guidelines)\n let gitDiff: string | undefined;\n let guidelines: string[] | undefined;\n try {\n const repoContext = await getRepoContext(directory, { diffBase });\n gitDiff = repoContext.gitDiff;\n guidelines = repoContext.guidelines;\n } catch {\n // Silently continue without repo context\n }\n\n const allFilenames = demoFiles.map((d) => d.filename);\n const prompt = buildReviewPrompt({ filenames: allFilenames, stepsMap: stepsMapByFilename, gitDiff, guidelines });\n\n const rawOutput = await invokeClaude(prompt, { agent });\n const llmResponse = parseLlmResponse(rawOutput);\n\n // Build a map of filename to type for easy lookup\n const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));\n\n // Construct final metadata by merging LLM summaries with steps and type\n // Convert filenames from LLM response to relative paths for proper video loading\n const metadata: ReviewMetadata = {\n demos: llmResponse.demos.map((demo) => {\n const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;\n return {\n file: relativePath,\n type: typeMap.get(demo.file) ?? \"web-ux\",\n summary: demo.summary,\n steps: stepsMapByRelativePath[relativePath] ?? [],\n };\n }),\n review: llmResponse.review,\n };\n\n const metadataPath = join(directory, \"review-metadata.json\");\n writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + \"\\n\");\n\n // Build app data and generate HTML\n // Videos are referenced by relative path in demo.file, no base64 encoding needed\n const appData: ReviewAppData = {\n metadata,\n title,\n videos: {},\n logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,\n feedbackEndpoint,\n };\n\n const html = generateReviewHtml(appData);\n const htmlPath = join(directory, \"review.html\");\n writeFileSync(htmlPath, html);\n\n return { htmlPath, metadataPath, metadata };\n}\n",
|
|
6
|
+
"export type SpawnFn = (\n cmd: string[],\n) => { exitCode: Promise<number>; stdout: ReadableStream<Uint8Array> };\n\nexport interface InvokeClaudeOptions {\n agent?: string;\n spawn?: SpawnFn;\n}\n\nconst GIT_DIFF_MAX_CHARS = 50_000;\n\nexport interface BuildReviewPromptOptions {\n filenames: string[];\n stepsMap: Record<string, Array<{ text: string; timestampSeconds: number }>>;\n gitDiff?: string;\n guidelines?: string[];\n}\n\nexport function buildReviewPrompt(options: BuildReviewPromptOptions): string {\n const { filenames, stepsMap, gitDiff, guidelines } = options;\n\n const demoEntries = filenames.map((f) => {\n const steps = stepsMap[f] ?? [];\n const stepLines = steps\n .map((s) => `- [${s.timestampSeconds}s] ${s.text}`)\n .join(\"\\n\");\n return `Video: ${f}\\nRecorded steps:\\n${stepLines || \"(no steps recorded)\"}`;\n });\n\n const sections: string[] = [];\n\n if (guidelines && guidelines.length > 0) {\n sections.push(`## Coding Guidelines\\n\\n${guidelines.join(\"\\n\\n\")}`);\n }\n\n if (gitDiff) {\n let diff = gitDiff;\n if (diff.length > GIT_DIFF_MAX_CHARS) {\n diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + \"\\n\\n... (diff truncated at 50k characters)\";\n }\n sections.push(`## Git Diff\\n\\n\\`\\`\\`diff\\n${diff}\\n\\`\\`\\``);\n }\n\n sections.push(`## Demo Recordings\\n\\n${demoEntries.join(\"\\n\\n\")}`);\n\n return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.\n\n${sections.join(\"\\n\\n\")}\n\n## Task\n\nReview the code changes and demo recordings. Generate a JSON object matching this exact schema:\n\n{\n \"demos\": [\n {\n \"file\": \"<filename>\",\n \"summary\": \"<a meaningful sentence describing what this demo showcases based on the steps>\"\n }\n ],\n \"review\": {\n \"summary\": \"<2-3 sentence overview of the changes>\",\n \"highlights\": [\"<positive aspect 1>\", \"<positive aspect 2>\"],\n \"verdict\": \"approve\" | \"request_changes\",\n \"verdictReason\": \"<one sentence justifying the verdict>\",\n \"issues\": [\n {\n \"severity\": \"major\" | \"minor\" | \"nit\",\n \"description\": \"<what the issue is and how to fix it>\"\n }\n ]\n }\n}\n\nRules:\n- Return ONLY the JSON object, no markdown fences or extra text.\n- Include one entry in \"demos\" for each filename, in the same order.\n- \"file\" must exactly match the provided filename.\n- \"verdict\" must be exactly \"approve\" or \"request_changes\".\n- Use \"request_changes\" if there are any \"major\" issues.\n- \"severity\" must be exactly \"major\", \"minor\", or \"nit\".\n- \"major\": bugs, security issues, broken functionality, guideline violations.\n- \"minor\": code quality, readability, missing edge cases.\n- \"nit\": style, naming, trivial improvements.\n- \"highlights\" must have at least one entry.\n- \"issues\" can be an empty array if there are no issues.\n- Verify that demo steps demonstrate the acceptance criteria being met.`;\n}\n\nexport async function invokeClaude(\n prompt: string,\n options?: InvokeClaudeOptions,\n): Promise<string> {\n const spawnFn = options?.spawn ?? defaultSpawn;\n const agent = options?.agent ?? \"claude\";\n const proc = spawnFn([agent, \"-p\", prompt]);\n\n const reader = proc.stdout.getReader();\n const chunks: Uint8Array[] = [];\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const exitCode = await proc.exitCode;\n const output = new TextDecoder().decode(\n concatUint8Arrays(chunks),\n );\n\n if (exitCode !== 0) {\n throw new Error(\n `claude process exited with code ${exitCode}: ${output.trim()}`,\n );\n }\n\n return output.trim();\n}\n\nimport type { IssueSeverity, ReviewVerdict } from \"./review-types.ts\";\n\nexport interface LlmReviewResponse {\n demos: Array<{ file: string; summary: string }>;\n review: {\n summary: string;\n highlights: string[];\n verdict: ReviewVerdict;\n verdictReason: string;\n issues: Array<{ severity: IssueSeverity; description: string }>;\n };\n}\n\nconst VALID_VERDICTS: ReadonlySet<string> = new Set([\"approve\", \"request_changes\"]);\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\"major\", \"minor\", \"nit\"]);\n\nexport function extractJson(raw: string): string {\n // Try raw string first\n try {\n JSON.parse(raw);\n return raw;\n } catch {\n // look for first { and last }\n }\n\n const start = raw.indexOf(\"{\");\n const end = raw.lastIndexOf(\"}\");\n if (start === -1 || end === -1 || end <= start) {\n throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);\n }\n\n return raw.slice(start, end + 1);\n}\n\nexport function parseLlmResponse(raw: string): LlmReviewResponse {\n const jsonStr = extractJson(raw);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonStr);\n } catch {\n throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);\n }\n\n if (typeof parsed !== \"object\" || parsed === null || !(\"demos\" in parsed)) {\n throw new Error(\"Missing 'demos' array in review metadata\");\n }\n\n const obj = parsed as Record<string, unknown>;\n if (!Array.isArray(obj[\"demos\"])) {\n throw new Error(\"'demos' must be an array\");\n }\n\n for (const demo of obj[\"demos\"] as unknown[]) {\n if (typeof demo !== \"object\" || demo === null) {\n throw new Error(\"Each demo must be an object\");\n }\n const d = demo as Record<string, unknown>;\n\n if (typeof d[\"file\"] !== \"string\") {\n throw new Error(\"Each demo must have a 'file' string\");\n }\n if (typeof d[\"summary\"] !== \"string\") {\n throw new Error(\"Each demo must have a 'summary' string\");\n }\n }\n\n if (typeof obj[\"review\"] !== \"object\" || obj[\"review\"] === null) {\n throw new Error(\"Missing 'review' object in response\");\n }\n\n const review = obj[\"review\"] as Record<string, unknown>;\n\n if (typeof review[\"summary\"] !== \"string\") {\n throw new Error(\"review.summary must be a string\");\n }\n\n if (!Array.isArray(review[\"highlights\"])) {\n throw new Error(\"review.highlights must be an array\");\n }\n if (review[\"highlights\"].length === 0) {\n throw new Error(\"review.highlights must not be empty\");\n }\n for (const h of review[\"highlights\"]) {\n if (typeof h !== \"string\") {\n throw new Error(\"Each highlight must be a string\");\n }\n }\n\n if (typeof review[\"verdict\"] !== \"string\" || !VALID_VERDICTS.has(review[\"verdict\"])) {\n throw new Error(\"review.verdict must be 'approve' or 'request_changes'\");\n }\n\n if (typeof review[\"verdictReason\"] !== \"string\") {\n throw new Error(\"review.verdictReason must be a string\");\n }\n\n if (!Array.isArray(review[\"issues\"])) {\n throw new Error(\"review.issues must be an array\");\n }\n\n for (const issue of review[\"issues\"] as unknown[]) {\n if (typeof issue !== \"object\" || issue === null) {\n throw new Error(\"Each issue must be an object\");\n }\n const i = issue as Record<string, unknown>;\n if (typeof i[\"severity\"] !== \"string\" || !VALID_SEVERITIES.has(i[\"severity\"])) {\n throw new Error(\"Each issue severity must be 'major', 'minor', or 'nit'\");\n }\n if (typeof i[\"description\"] !== \"string\") {\n throw new Error(\"Each issue must have a 'description' string\");\n }\n }\n\n return parsed as LlmReviewResponse;\n}\n\nimport { spawn } from \"node:child_process\";\nimport { Readable } from \"node:stream\";\n\nfunction defaultSpawn(\n cmd: string[],\n): { exitCode: Promise<number>; stdout: ReadableStream<Uint8Array> } {\n const [command, ...args] = cmd;\n const proc = spawn(command!, args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const exitCode = new Promise<number>((resolve) => {\n proc.on(\"close\", (code) => resolve(code ?? 1));\n });\n\n const stdout = Readable.toWeb(proc.stdout!) as unknown as ReadableStream<Uint8Array>;\n\n return { exitCode, stdout };\n}\n\nfunction concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const a of arrays) {\n result.set(a, offset);\n offset += a.length;\n }\n return result;\n}\n",
|
|
7
|
+
"import { readFileSync } from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\n\nexport type ExecFn = (cmd: string[], cwd: string) => Promise<string>;\nexport type ReadFileFn = (path: string) => string;\n\nexport interface RepoContext {\n gitDiff: string;\n guidelines: string[];\n}\n\nexport interface GetRepoContextOptions {\n exec?: ExecFn;\n readFile?: ReadFileFn;\n diffBase?: string; // Base commit/branch for diff (auto-detected if not provided)\n}\n\nasync function detectDefaultBase(exec: ExecFn, gitRoot: string): Promise<string | null> {\n // Get current branch name\n let currentBranch: string;\n try {\n currentBranch = (await exec([\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\"], gitRoot)).trim();\n } catch {\n return null; // Detached HEAD or other issue\n }\n\n // If on main/master, no base to compare against\n if (currentBranch === \"main\" || currentBranch === \"master\") {\n return null;\n }\n\n // Try to find main or master as base\n for (const candidate of [\"main\", \"master\"]) {\n try {\n await exec([\"git\", \"rev-parse\", \"--verify\", candidate], gitRoot);\n return candidate;\n } catch {\n // Branch doesn't exist, try next\n }\n }\n\n return null;\n}\n\nconst defaultExec: ExecFn = async (cmd, cwd) => {\n const [command, ...args] = cmd;\n const proc = spawnSync(command!, args, { cwd, encoding: \"utf-8\" });\n if (proc.status !== 0) {\n const stderr = (proc.stderr ?? \"\").trim();\n throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(\" \")}${stderr ? `: ${stderr}` : \"\"}`);\n }\n return proc.stdout ?? \"\";\n};\n\nconst defaultReadFile: ReadFileFn = (path) => {\n return readFileSync(path, \"utf-8\");\n};\n\nexport async function getRepoContext(\n demosDir: string,\n options?: GetRepoContextOptions,\n): Promise<RepoContext> {\n const exec = options?.exec ?? defaultExec;\n const readFile = options?.readFile ?? defaultReadFile;\n\n const gitRoot = (await exec([\"git\", \"rev-parse\", \"--show-toplevel\"], demosDir)).trim();\n\n // Determine the base for diff comparison\n const diffBase = options?.diffBase ?? await detectDefaultBase(exec, gitRoot);\n\n let gitDiff: string;\n if (diffBase) {\n // Use three-dot diff for merge-base comparison (shows changes on current branch)\n gitDiff = (await exec([\"git\", \"diff\", `${diffBase}...HEAD`], gitRoot)).trim();\n } else {\n // Fallback: worktree diff or last commit\n const workingDiff = (await exec([\"git\", \"diff\", \"HEAD\"], gitRoot)).trim();\n if (workingDiff.length > 0) {\n gitDiff = workingDiff;\n } else {\n gitDiff = (await exec([\"git\", \"diff\", \"HEAD~1..HEAD\"], gitRoot)).trim();\n }\n }\n\n const lsOutput = (await exec([\"git\", \"ls-files\"], gitRoot)).trim();\n const files = lsOutput.split(\"\\n\").filter((f) => f.length > 0);\n\n const guidelinePatterns = [\"CLAUDE.md\", \"SKILL.md\"];\n const guidelines: string[] = [];\n\n for (const file of files) {\n const basename = file.split(\"/\").pop() ?? \"\";\n if (guidelinePatterns.includes(basename)) {\n const fullPath = `${gitRoot}/${file}`;\n const content = readFile(fullPath);\n guidelines.push(`# ${file}\\n${content}`);\n }\n }\n\n return { gitDiff, guidelines };\n}\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "D519E984A168289764756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|