Render structured project review results

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-26 23:23:08 -07:00
parent 0c0ba88fdb
commit 41b3aed3ed
5 changed files with 197 additions and 19 deletions

View File

@@ -7,10 +7,17 @@ export type ProjectReviewCheckDefinition = {
export type ProjectReviewCheckConclusion = "approved" | "fix-recommended";
export type ProjectReviewCheckFinding = {
title: string;
detail: string | null;
file: string | null;
line: number | null;
};
export type ProjectReviewCheckResult = {
conclusion: ProjectReviewCheckConclusion;
summary: string;
findings: string[];
findings: ProjectReviewCheckFinding[];
};
export const PROJECT_REVIEW_CHECK_RESULT_MARKER: string;

View File

@@ -144,6 +144,26 @@ function normalizedConclusion(value) {
return null;
}
function normalizedFinding(value) {
if (typeof value === "string") {
const title = normalizedText(value, 240);
return title ? { title, detail: null, file: null, line: null } : null;
}
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const title = normalizedText(value.title, 240);
if (!title) return null;
const line =
Number.isInteger(value.line) && value.line > 0 && value.line <= 10_000_000
? value.line
: null;
return {
title,
detail: normalizedText(value.detail, 1_000),
file: normalizedText(value.file, 500),
line,
};
}
function jsonObjectsAfterMarker(content) {
const markerIndex = content.lastIndexOf(PROJECT_REVIEW_CHECK_RESULT_MARKER);
if (markerIndex < 0) return [];
@@ -177,10 +197,7 @@ export function parseProjectReviewCheckResult(content) {
const summary = normalizedText(parsed.summary, 4_000);
if (!conclusion || !summary) continue;
const findings = Array.isArray(parsed.findings)
? parsed.findings
.map((finding) => normalizedText(finding, 1_000))
.filter(Boolean)
.slice(0, 20)
? parsed.findings.map(normalizedFinding).filter(Boolean).slice(0, 20)
: [];
return { conclusion, summary, findings };
} catch {
@@ -222,7 +239,9 @@ export function buildProjectReviewCheckPrompt({
"",
"End your response with exactly this marker and one JSON object:",
PROJECT_REVIEW_CHECK_RESULT_MARKER,
'{"conclusion":"approved|fix-recommended","summary":"short explanation","findings":["actionable finding"]}',
'{"conclusion":"approved|fix-recommended","summary":"short explanation","findings":[{"title":"concise actionable fix","detail":"why this matters and what to change","file":"relative/path.ext","line":123}]}',
"Return an empty findings array when approved. For every recommended fix, return one finding object.",
"Use null for file or line when the finding is not tied to a precise source location.",
"Use approved only when no material fix is recommended. Use fix-recommended otherwise.",
].join("\n");
}

View File

@@ -89,7 +89,62 @@ test("normalizes a changes-requested result to fix-recommended", () => {
assert.deepEqual(result, {
conclusion: "fix-recommended",
summary: "The empty state has no accessible name.",
findings: ["Add an accessible label.", "Cover it with a UI test."],
findings: [
{
title: "Add an accessible label.",
detail: null,
file: null,
line: null,
},
{
title: "Cover it with a UI test.",
detail: null,
file: null,
line: null,
},
],
});
});
test("parses structured findings with bounded source locations", () => {
const result = parseProjectReviewCheckResult(
`${PROJECT_REVIEW_CHECK_RESULT_MARKER}\n${JSON.stringify({
conclusion: "fix-recommended",
summary: "Two interface fixes are needed.",
findings: [
{
title: "Use the shared button primitive",
detail: "The custom button misses the standard focus treatment.",
file: "desktop/src/features/projects/ui/ProjectReviewChecks.tsx",
line: 742,
},
{
title: "Add an accessible name",
detail: "Icon-only controls need an explicit label.",
file: null,
line: -1,
},
],
})}`,
);
assert.deepEqual(result, {
conclusion: "fix-recommended",
summary: "Two interface fixes are needed.",
findings: [
{
title: "Use the shared button primitive",
detail: "The custom button misses the standard focus treatment.",
file: "desktop/src/features/projects/ui/ProjectReviewChecks.tsx",
line: 742,
},
{
title: "Add an accessible name",
detail: "Icon-only controls need an explicit label.",
file: null,
line: null,
},
],
});
});

View File

@@ -353,6 +353,74 @@ function CheckStatus({
return null;
}
function ReviewCheckResultCard({
result,
}: {
result: ProjectReviewCheckResult;
}) {
const isApproved = result.conclusion === "approved";
const findingCount = result.findings.length;
const title = isApproved
? "Approved"
: findingCount === 0
? "Fix recommended"
: `${findingCount} ${findingCount === 1 ? "fix" : "fixes"} recommended`;
return (
<section
className="overflow-hidden rounded-xl border border-border/70 bg-background/70"
data-testid="project-review-check-result"
>
<div className="flex items-start gap-3 px-4 py-3.5">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted/70">
{isApproved ? (
<CheckCircle2 className="h-5 w-5 text-emerald-500" />
) : (
<CircleAlert className="h-5 w-5 text-amber-500" />
)}
</div>
<div className="min-w-0 flex-1">
<h5 className="text-sm font-semibold text-foreground">{title}</h5>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{result.summary}
</p>
</div>
</div>
{findingCount > 0 ? (
<div className="divide-y divide-border/55 border-t border-border/55">
{result.findings.map((finding) => (
<article
className="px-4 py-3"
data-testid="project-review-check-finding"
key={JSON.stringify(finding)}
>
<div className="flex items-start justify-between gap-4">
<p className="min-w-0 flex-1 text-xs font-medium text-foreground">
{finding.title}
</p>
{finding.file ? (
<span
className="max-w-[45%] truncate font-mono text-2xs text-muted-foreground"
title={`${finding.file}${finding.line ? `:${finding.line}` : ""}`}
>
{finding.file}
{finding.line ? `:${finding.line}` : ""}
</span>
) : null}
</div>
{finding.detail ? (
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{finding.detail}
</p>
) : null}
</article>
))}
</div>
) : null}
</section>
);
}
function AgentPicker({
candidates,
disabled,
@@ -763,18 +831,7 @@ export function ProjectReviewChecks({
<p className="text-xs text-destructive">{run.error}</p>
) : null}
{run.result ? (
<div className="rounded-lg border border-border/55 bg-muted/25 p-3">
<p className="text-sm text-foreground">
{run.result.summary}
</p>
{run.result.findings.length > 0 ? (
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
{run.result.findings.map((finding) => (
<li key={finding}>{finding}</li>
))}
</ul>
) : null}
</div>
<ReviewCheckResultCard result={run.result} />
) : null}
{run.status === "running" && run.opener && run.agentPubkey ? (
<ReviewCheckResultObserver

View File

@@ -172,6 +172,46 @@ test("review checks dispatch to an agent authorized for the relay identity", asy
),
)
.toBe(true);
await waitForMockLiveSubscription(page, "DM");
await page.evaluate((agentPubkey) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "DM",
content: `BUZZ_CHECK_RESULT_V1\n${JSON.stringify({
conclusion: "fix-recommended",
summary: "Two interface fixes are needed before approval.",
findings: [
{
title: "Use the shared button primitive",
detail: "The custom control misses the standard focus treatment.",
file: "desktop/src/features/projects/ui/ProjectReviewChecks.tsx",
line: 742,
},
{
title: "Cover the disabled interaction state",
detail: "Add a UI assertion for the pending check state.",
file: "desktop/tests/e2e/project-pr-review.spec.ts",
line: 118,
},
],
})}`,
createdAt: Math.floor(Date.now() / 1_000) + 1,
kind: 9,
pubkey: agentPubkey,
});
}, RELAY_REVIEW_AGENT_PUBKEY);
const result = interfaceCheck.getByTestId("project-review-check-result");
await expect(result.getByRole("heading")).toHaveText("2 fixes recommended");
await expect(result).toContainText(
"Two interface fixes are needed before approval.",
);
await expect(result.getByTestId("project-review-check-finding")).toHaveCount(
2,
);
await expect(result).toContainText("ProjectReviewChecks.tsx:742");
await waitForAnimations(page);
await result.screenshot({ path: `${SHOTS}/review-check-result.png` });
});
test("review checks dispatch to a running local managed agent", async ({