From b03544775c911ec0a8a2ab8016d3a5718eb307f5 Mon Sep 17 00:00:00 2001
From: Taras Mankovski <74687+taras@users.noreply.github.com>
Date: Sat, 8 Aug 2026 06:17:22 -0400
Subject: [PATCH 01/13] Refactor review infrastructure around typed components
---
.github/workflows/repo-analysis.yml | 6 +-
.github/workflows/review.yml | 6 +-
.reviews/AnalyzeRepo.md | 175 ++--------
.reviews/AnalyzeRepoCI.md | 171 ++--------
.reviews/ReviewPR.local.md | 203 ++----------
.reviews/ReviewPR.md | 207 ++----------
.reviews/components/CleanupIssues.md | 38 ++-
.reviews/components/CommentReview.md | 245 +-------------
.reviews/components/CommentReviewData.ts | 286 ++++++++++++++++
.reviews/components/CommentReviewState.ts | 200 ++++++++++++
.reviews/components/DeepInfraProvider.md | 14 +-
.reviews/components/Doctor.md | 239 --------------
.reviews/components/Doctor.ts | 171 ++++++++++
.reviews/components/EnsureOxlint.md | 106 +++---
.reviews/components/GitHubAuth.md | 46 +++
.reviews/components/GitHubComment.md | 50 ++-
.reviews/components/OllamaProvider.md | 6 +-
.reviews/components/OxlintConfig.md | 34 ++
.reviews/components/OxlintDiagnostics.ts | 74 +++++
.reviews/components/RepositoryInventory.ts | 48 +++
.reviews/components/ReviewContext.ts | 78 +++++
.reviews/components/ReviewSetup.md | 30 ++
.reviews/components/SuggestRemoval.md | 41 ++-
.reviews/components/Threshold.md | 2 +-
packages/code-review-agent/mod.ts | 6 +-
packages/code-review-agent/src/doctor.ts | 98 ++++++
.../src/parse-diagnostics.ts | 128 +-------
.../code-review-agent/src/parse-oxlint.ts | 100 ++++++
.../code-review-agent/tests/doctor.test.ts | 60 ++++
.../tests/parse-diagnostics.test.ts | 19 +-
.../tests/parse-oxlint.test.ts | 45 +++
scripts/tests/review-infrastructure.test.ts | 307 ++++++++++++++++++
specs/code-review-agent-spec.md | 160 ++++-----
specs/oxlint-sensor-spec.md | 168 +++++++---
specs/release-process-spec.md | 27 +-
35 files changed, 2081 insertions(+), 1513 deletions(-)
create mode 100644 .reviews/components/CommentReviewData.ts
create mode 100644 .reviews/components/CommentReviewState.ts
delete mode 100644 .reviews/components/Doctor.md
create mode 100644 .reviews/components/Doctor.ts
create mode 100644 .reviews/components/GitHubAuth.md
create mode 100644 .reviews/components/OxlintConfig.md
create mode 100644 .reviews/components/OxlintDiagnostics.ts
create mode 100644 .reviews/components/RepositoryInventory.ts
create mode 100644 .reviews/components/ReviewContext.ts
create mode 100644 .reviews/components/ReviewSetup.md
create mode 100644 packages/code-review-agent/src/doctor.ts
create mode 100644 packages/code-review-agent/src/parse-oxlint.ts
create mode 100644 packages/code-review-agent/tests/doctor.test.ts
create mode 100644 packages/code-review-agent/tests/parse-oxlint.test.ts
create mode 100644 scripts/tests/review-infrastructure.test.ts
diff --git a/.github/workflows/repo-analysis.yml b/.github/workflows/repo-analysis.yml
index 8520c9e5..82b27451 100644
--- a/.github/workflows/repo-analysis.yml
+++ b/.github/workflows/repo-analysis.yml
@@ -29,12 +29,14 @@ jobs:
ref: ${{ inputs.ref }}
fetch-depth: 0
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
+
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- - name: Install dependencies
- run: deno task deps
+ - name: Prepare the checked-out xmd binary
+ run: deno task setup
- name: Build the checked-out xmd binary
run: deno task build
diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml
index 3abbda17..cf3cc40d 100644
--- a/.github/workflows/review.yml
+++ b/.github/workflows/review.yml
@@ -15,12 +15,14 @@ jobs:
with:
fetch-depth: 0
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
+
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- - name: Install dependencies
- run: deno task deps
+ - name: Prepare the checked-out xmd binary
+ run: deno task setup
- name: Build the checked-out xmd binary
run: deno task build
diff --git a/.reviews/AnalyzeRepo.md b/.reviews/AnalyzeRepo.md
index ed8e7ced..e10db393 100644
--- a/.reviews/AnalyzeRepo.md
+++ b/.reviews/AnalyzeRepo.md
@@ -2,171 +2,42 @@
title: Repository Analysis
---
-```bash silent exec
-mkdir -p .reviews
-cat > .reviews/tsconfig.oxlint.json << 'TSCONFIG'
-{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "strict": true,
- "noEmit": true,
- "skipLibCheck": true,
- "resolveJsonModule": true,
- "lib": ["ESNext", "DOM"],
- "types": []
- },
- "include": ["packages/*/src/**/*.ts", "packages/*/*.ts", "durable-effects/**/*.ts"],
- "exclude": ["node_modules", "dist", ".vendor", "**/*.test.ts"]
-}
-TSCONFIG
-```
-
-```bash silent exec
-ollama show qwen3:30b-a3b >/dev/null 2>&1 || ollama pull qwen3:30b-a3b
-```
+
+
-
-
-```bash exec
-find durable-effects packages -name '*.ts' -not -name '*.test.ts' -not -name '*.spec.ts' -not -path '*/node_modules/*' 2>/dev/null | tee /tmp/xmd-repo-files.txt | wc -l | tr -d ' '
-```
-
-
-
-
-
-```bash exec
-cat /tmp/xmd-repo-files.txt | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}'
-```
-
-
-
-
-
-```bash exec
-cat /tmp/xmd-repo-files.txt
-```
-
-
+
```ts eval
-const fileCount = parseInt(repoStats.trim(), 10) || 0;
-const lineCount = parseInt(repoLineCount.trim(), 10) || 0;
-
+const fileList = inventory.fileList;
+const fileCount = inventory.fileCount;
+const lineCount = inventory.lineCount;
const pr = {
- files: [], added: [], removed: [], created: [], modified: [], deleted: [],
+ files: [],
+ added: [],
+ removed: [],
+ created: [],
+ modified: [],
+ deleted: [],
directories: new Set(),
- addedSource: "", diffPreview: "",
+ addedSource: "",
+ diffPreview: "",
stats: { totalFiles: fileCount, additions: lineCount, deletions: 0, totalChanges: lineCount },
meta: { title: "Repo Analysis", body: "", number: "" },
};
```
-
-
-
-
-
-
-```ts eval
-import { parseDoctorResult } from "@executablemd/code-review-agent";
-
-const doctor = parseDoctorResult(doctorJson);
-```
-
-
-
-
-
-```bash exec
-OUT=$(OXLINT_TSGOLINT_PATH=.reviews/.oxlint/tsgolint .reviews/.oxlint/oxlint --config .reviews/.oxlintrc.json --type-aware --tsconfig .reviews/tsconfig.oxlint.json --format json 2>/dev/null || true)
-if [ -z "$OUT" ] || ! printf '%s' "$OUT" | jq -c '
- def entries:
- if type == "array" then .
- elif (.diagnostics? | type) == "array" then .diagnostics
- else []
- end;
- def span_line:
- if (.line? | type) == "number" then .line
- elif (.labels?[0].span.line? | type) == "number" then .labels[0].span.line
- else 0
- end;
- def span_column:
- if (.column? | type) == "number" then .column
- elif (.labels?[0].span.column? | type) == "number" then .labels[0].span.column
- else 0
- end;
- entries
- | map({
- message: (if (.message? | type) == "string" then .message else "" end),
- ruleId: (if (.ruleId? | type) == "string" then .ruleId elif (.code? | type) == "string" then .code else "unknown" end),
- severity: (if .severity == "error" then "error" else "warning" end),
- file: (if (.file? | type) == "string" then .file elif (.filename? | type) == "string" then .filename else "" end),
- line: span_line,
- column: span_column
- })
-'; then
- printf '[]'
-fi
-```
-
-
+
-
-
-```bash exec
-OUT=$(.reviews/.oxlint/oxlint --config .reviews/.oxlintrc.json --format json 2>/dev/null || true)
-if [ -z "$OUT" ] || ! printf '%s' "$OUT" | jq -c '
- def entries:
- if type == "array" then .
- elif (.diagnostics? | type) == "array" then .diagnostics
- else []
- end;
- def span_line:
- if (.line? | type) == "number" then .line
- elif (.labels?[0].span.line? | type) == "number" then .labels[0].span.line
- else 0
- end;
- def span_column:
- if (.column? | type) == "number" then .column
- elif (.labels?[0].span.column? | type) == "number" then .labels[0].span.column
- else 0
- end;
- entries
- | map({
- message: (if (.message? | type) == "string" then .message else "" end),
- ruleId: (if (.ruleId? | type) == "string" then .ruleId elif (.code? | type) == "string" then .code else "unknown" end),
- severity: (if .severity == "error" then "error" else "warning" end),
- file: (if (.file? | type) == "string" then .file elif (.filename? | type) == "string" then .filename else "" end),
- line: span_line,
- column: span_column
- })
-'; then
- printf '[]'
-fi
-```
-
-
-
-
-
-[]
-
-
-
-
+
```ts eval
-import {
- buildCleanupAnalysis,
- parseDiagnostics,
-} from "@executablemd/code-review-agent";
+import { buildCleanupAnalysis, buildDiagnostics } from "@executablemd/code-review-agent";
-const diagnostics = parseDiagnostics(rawDiagnostics, pr, doctor);
+const diagnostics = buildDiagnostics(rawDiagnostics, pr, doctor);
const cleanupAnalysis = buildCleanupAnalysis(diagnostics);
```
@@ -177,3 +48,5 @@ const cleanupAnalysis = buildCleanupAnalysis(diagnostics);
+
+
diff --git a/.reviews/AnalyzeRepoCI.md b/.reviews/AnalyzeRepoCI.md
index 90539595..4ee99b25 100644
--- a/.reviews/AnalyzeRepoCI.md
+++ b/.reviews/AnalyzeRepoCI.md
@@ -4,167 +4,42 @@ title: Repository Analysis (CI)
diff --git a/.reviews/ReviewPR.local.md b/.reviews/ReviewPR.local.md
index c852eab9..6a4a21da 100644
--- a/.reviews/ReviewPR.local.md
+++ b/.reviews/ReviewPR.local.md
@@ -2,198 +2,31 @@
title: PR Review (local)
---
-```ts eval
-const BASE_SHA = process.env.BASE_SHA ?? "HEAD~1";
-const HEAD_SHA = process.env.HEAD_SHA ?? "HEAD";
-const PR_TITLE = process.env.PR_TITLE ?? "";
-const PR_BODY = process.env.PR_BODY ?? "";
-const PR_NUMBER = process.env.PR_NUMBER ?? "";
-```
-
-
-
-```bash exec
-git diff {BASE_SHA}...{HEAD_SHA}
-```
+
+
-
-
-
-
-```bash exec
-git diff --name-status {BASE_SHA}...{HEAD_SHA}
-```
-
-
+
+
```ts eval
-import { parseDiff } from "@executablemd/code-review-agent";
-
-const pr = parseDiff(rawDiff, rawFiles, {
- title: PR_TITLE,
- body: PR_BODY,
- number: PR_NUMBER,
-});
-
-// TODO: we need an easier way to work with diffs here.
-const changedFilePaths = pr.files.map((file) => file.path);
-```
-
-```bash silent exec
-mkdir -p .reviews
-cat > .reviews/tsconfig.oxlint.json << 'TSCONFIG'
-{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "strict": true,
- "noEmit": true,
- "skipLibCheck": true,
- "resolveJsonModule": true,
- "lib": ["ESNext", "DOM"],
- "types": []
- },
- "include": [
- "packages/*/src/**/*.ts",
- "packages/*/*.ts",
- "durable-effects/**/*.ts"
- ],
- "exclude": ["node_modules", "dist", ".vendor", "**/*.test.ts"]
-}
-TSCONFIG
-```
-
-```bash silent exec
-ollama show qwen3:30b-a3b >/dev/null 2>&1 || ollama pull qwen3:30b-a3b
+const pr = context.pr;
+const changedFilePaths = context.changedFilePaths;
+const changedTsFiles = pr.files
+ .filter((file) => file.language === "typescript" && !file.isTest && !file.isTypeDeclaration)
+ .map((file) => file.path)
+ .slice(0, 200);
```
-
-
-
-
-
+
```ts eval
-import { parseDoctorResult } from "@executablemd/code-review-agent";
-
-const doctor = parseDoctorResult(doctorJson);
-```
-
-
+import { buildDiagnostics } from "@executablemd/code-review-agent";
-```bash silent exec
-git diff --name-only {BASE_SHA}...{HEAD_SHA} -- '*.ts' '*.tsx' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '\.d\.ts$' | head -200
-```
-
-
-
-
-
-
-
-```bash exec
-changed_files=$(cat <<'FILES'
-{changedTsFiles}
-FILES
-)
-if [ -z "$changed_files" ]; then
- printf '[]'
-else
- raw=$(printf '%s\n' "$changed_files" | OXLINT_TSGOLINT_PATH=.reviews/.oxlint/tsgolint xargs .reviews/.oxlint/oxlint --config .reviews/.oxlintrc.json --type-aware --tsconfig .reviews/tsconfig.oxlint.json --format json 2>/dev/null || true)
- if [ -z "$raw" ] || ! printf '%s' "$raw" | jq -c --arg changed "$changed_files" '
- def entries:
- if type == "array" then .
- elif (.diagnostics? | type) == "array" then .diagnostics
- else []
- end;
- def span_line:
- if (.line? | type) == "number" then .line
- elif (.labels?[0].span.line? | type) == "number" then .labels[0].span.line
- else 0
- end;
- def span_column:
- if (.column? | type) == "number" then .column
- elif (.labels?[0].span.column? | type) == "number" then .labels[0].span.column
- else 0
- end;
- entries
- | map({
- message: (if (.message? | type) == "string" then .message else "" end),
- ruleId: (if (.ruleId? | type) == "string" then .ruleId elif (.code? | type) == "string" then .code else "unknown" end),
- severity: (if .severity == "error" then "error" else "warning" end),
- file: (if (.file? | type) == "string" then .file elif (.filename? | type) == "string" then .filename else "" end),
- line: span_line,
- column: span_column
- })
- | map(select(.file as $file | ($changed | split("\n") | index($file)) != null))
- '; then
- printf '[]'
- fi
-fi
-```
-
-
-
-
-
-```bash exec
-changed_files=$(cat <<'FILES'
-{changedTsFiles}
-FILES
-)
-if [ -z "$changed_files" ]; then
- printf '[]'
-else
- raw=$(printf '%s\n' "$changed_files" | xargs .reviews/.oxlint/oxlint --config .reviews/.oxlintrc.json --format json 2>/dev/null || true)
- if [ -z "$raw" ] || ! printf '%s' "$raw" | jq -c --arg changed "$changed_files" '
- def entries:
- if type == "array" then .
- elif (.diagnostics? | type) == "array" then .diagnostics
- else []
- end;
- def span_line:
- if (.line? | type) == "number" then .line
- elif (.labels?[0].span.line? | type) == "number" then .labels[0].span.line
- else 0
- end;
- def span_column:
- if (.column? | type) == "number" then .column
- elif (.labels?[0].span.column? | type) == "number" then .labels[0].span.column
- else 0
- end;
- entries
- | map({
- message: (if (.message? | type) == "string" then .message else "" end),
- ruleId: (if (.ruleId? | type) == "string" then .ruleId elif (.code? | type) == "string" then .code else "unknown" end),
- severity: (if .severity == "error" then "error" else "warning" end),
- file: (if (.file? | type) == "string" then .file elif (.filename? | type) == "string" then .filename else "" end),
- line: span_line,
- column: span_column
- })
- | map(select(.file as $file | ($changed | split("\n") | index($file)) != null))
- '; then
- printf '[]'
- fi
-fi
-```
-
-
-
-
-
-[]
-
-
-
-
-
-```ts eval
-import { parseDiagnostics } from "@executablemd/code-review-agent";
-
-const diagnostics = parseDiagnostics(rawDiagnostics, pr, doctor);
+const diagnostics = buildDiagnostics(rawDiagnostics, pr, doctor);
```
@@ -205,3 +38,5 @@ const diagnostics = parseDiagnostics(rawDiagnostics, pr, doctor);
+
+
diff --git a/.reviews/ReviewPR.md b/.reviews/ReviewPR.md
index ecea60f1..7d8063fc 100644
--- a/.reviews/ReviewPR.md
+++ b/.reviews/ReviewPR.md
@@ -4,202 +4,31 @@ title: PR Review
diff --git a/.reviews/components/CleanupIssues.md b/.reviews/components/CleanupIssues.md
index 110458b9..8ff93ca9 100644
--- a/.reviews/components/CleanupIssues.md
+++ b/.reviews/components/CleanupIssues.md
@@ -11,31 +11,34 @@ props:
---
```ts persist eval
-const repo = process.env.GITHUB_REPOSITORY;
-
-function githubHeaders() {
- return {
- "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`,
- "Accept": "application/vnd.github+json",
- "Content-Type": "application/json",
- };
+import { env as runtimeEnv } from "@executablemd/runtime";
+
+function* githubConfiguration() {
+ const token = yield* runtimeEnv("GITHUB_TOKEN");
+ const repo = yield* runtimeEnv("GITHUB_REPOSITORY");
+ if (!token || !repo) {
+ return undefined;
+ }
+ const [owner, repoName] = repo.split("/");
+ return { api: `https://api.github.com/repos/${owner}/${repoName}` };
}
-if (!process.env.GITHUB_TOKEN || !repo) {
+const github = yield* githubConfiguration();
+if (!github) {
return "";
}
-const [owner, repoName] = repo.split("/");
-const api = `https://api.github.com/repos/${owner}/${repoName}`;
+const { api } = github;
+
const LABEL = "cleanup";
const TOP_N = 5;
// 1. Ensure label exists
-const labelResponse = yield* fetch(`${api}/labels/${LABEL}`, { headers: githubHeaders() });
+const labelResponse = yield* fetch(`${api}/labels/${LABEL}`);
if (labelResponse.status === 404) {
yield* fetch(`${api}/labels`, {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: LABEL,
description: "Auto-generated cleanup finding from repo analysis",
@@ -50,7 +53,6 @@ let page = 1;
while (true) {
const batch = yield* fetch(
`${api}/issues?labels=${LABEL}&state=open&per_page=100&page=${page}`,
- { headers: githubHeaders() },
).expect().json();
if (!Array.isArray(batch) || batch.length === 0) break;
@@ -109,14 +111,14 @@ for (const cluster of topClusters) {
if (existing) {
yield* fetch(api + "/issues/" + existing.number, {
method: "PATCH",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body }),
}).expect();
updated++;
} else {
yield* fetch(api + "/issues", {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title,
body,
@@ -132,7 +134,7 @@ for (const [file, issue] of issuesByFile.entries()) {
if (!topFiles.has(file)) {
yield* fetch(api + "/issues/" + issue.number + "/comments", {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
body: "Resolved — file no longer in top-5 cleanup targets. Closing automatically.",
}),
@@ -140,7 +142,7 @@ for (const [file, issue] of issuesByFile.entries()) {
yield* fetch(api + "/issues/" + issue.number, {
method: "PATCH",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state: "closed" }),
}).expect();
closed++;
diff --git a/.reviews/components/CommentReview.md b/.reviews/components/CommentReview.md
index 944b2eaf..0619d620 100644
--- a/.reviews/components/CommentReview.md
+++ b/.reviews/components/CommentReview.md
@@ -8,126 +8,10 @@ props:
additionalProperties: false
---
-```ts eval
-// ---------------------------------------------------------------------------
-// 1. Build comment/code pairs with file/line metadata
-const pairs = [];
-const lines = props.pr.added.filter(l => !l.isTest);
-
-for (let i = 0; i < lines.length - 1; i++) {
- const current = lines[i].content.trim();
- const next = lines[i + 1].content.trim();
- if (current.startsWith("//") && !next.startsWith("//") && next.length > 0) {
- pairs.push({
- comment: current,
- code: next,
- file: lines[i].file,
- lineNumber: lines[i].lineNumber,
- });
- }
-}
-
-const hasPairs = pairs.length >= 3;
-const pairsText = hasPairs
- ? pairs.map((p, i) =>
- `[${i}] COMMENT: ${p.comment}\nCODE: ${p.code}`
- ).join("\n---\n")
- : "";
-
-let hasChecklist = false;
-let checklistMd = "";
-
-// ---------------------------------------------------------------------------
-// 2. Fetch previous bot review comments and human replies
-const repo = process.env.GITHUB_REPOSITORY;
-const prNumber = process.env.PR_NUMBER;
-
-function githubHeaders() {
- return {
- "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`,
- "Accept": "application/vnd.github+json",
- };
-}
-
-let previousFindings = [];
-let dismissedReplies = [];
-let repliesForClassification = [];
-
-if (process.env.GITHUB_TOKEN && repo && prNumber) {
- const [owner, name] = repo.split("/");
- const api = `https://api.github.com/repos/${owner}/${name}`;
-
- const allComments = yield* fetch(
- `${api}/pulls/${prNumber}/comments?per_page=100`, { headers: githubHeaders() }
- ).expect().json();
-
- const botComments = allComments.filter(c =>
- c.user.login === "github-actions[bot]" &&
- c.body && c.body.includes("Redundant comment")
- );
-
- // Build map of bot comment id → { file, line, comment }
- const botCommentMap = new Map();
- for (const bc of botComments) {
- // Extract the comment text from the diff hunk (last + line with //)
- const hunkLines = (bc.diff_hunk ?? "").split("\n");
- const commentLine = hunkLines.filter(l => l.startsWith("+")).pop() ?? "";
- const commentText = commentLine.replace(/^\+\s*/, "").trim();
- botCommentMap.set(bc.id, {
- file: bc.path,
- lineNumber: bc.original_line ?? bc.line,
- comment: commentText,
- });
- }
-
- const humanReplies = allComments.filter(c =>
- c.in_reply_to_id && botCommentMap.has(c.in_reply_to_id) &&
- c.user.type !== "Bot"
- );
-
- // Check which replies already have a 👍 reaction (already processed)
- for (const reply of humanReplies) {
- const location = botCommentMap.get(reply.in_reply_to_id);
- const entry = {
- ...location,
- botCommentId: reply.in_reply_to_id,
- replyText: reply.body,
- replyId: reply.id,
- };
- try {
- const reactions = yield* fetch(
- `${api}/pulls/comments/${reply.id}/reactions`, { headers: githubHeaders() }
- ).expect().json();
- const alreadyAcked = reactions.some(r =>
- r.user.login === "github-actions[bot]" && r.content === "+1"
- );
- if (alreadyAcked) {
- dismissedReplies.push({ ...entry, alreadyProcessed: true });
- } else {
- repliesForClassification.push(entry);
- }
- } catch {
- repliesForClassification.push(entry);
- }
- }
-
- previousFindings = botComments.map(bc => ({
- file: bc.path,
- lineNumber: bc.original_line ?? bc.line,
- }));
-}
-
-const hasRepliesToClassify = repliesForClassification.length > 0;
-const repliesText = hasRepliesToClassify
- ? repliesForClassification.map((r, i) =>
- `[${i}] FILE: ${r.file}:${r.lineNumber}\nREPLY: "${r.replyText}"`
- ).join("\n---\n")
- : "";
-```
-
-
+
+
@@ -140,52 +24,15 @@ ACCEPT — the user agrees the comment should be removed
Format: [index] DISMISS or [index] ACCEPT
-{repliesText}
+{reviewData.repliesText}
-
-
-```ts eval
-const classPattern = /\[(\d+)\]\s*(DISMISS|ACCEPT)/gi;
-let cm;
-while ((cm = classPattern.exec(classificationResult)) !== null) {
- const idx = parseInt(cm[1], 10);
- const intent = cm[2].toUpperCase();
- if (idx >= 0 && idx < repliesForClassification.length && intent === "DISMISS") {
- dismissedReplies.push(repliesForClassification[idx]);
- }
-}
-```
-
-
-```ts eval
-// ---------------------------------------------------------------------------
-// 3. Build dismissed set and detect applied suggestions
-// ---------------------------------------------------------------------------
-
-const dismissedSet = new Set(
- dismissedReplies.map(d => `${d.file}:${d.lineNumber}`)
-);
-
-const addedLineSet = new Set(
- props.pr.added.map(l => `${l.file}:${l.lineNumber}`)
-);
-const appliedFindings = previousFindings.filter(pf =>
- pf.lineNumber && !addedLineSet.has(`${pf.file}:${pf.lineNumber}`) &&
- !dismissedSet.has(`${pf.file}:${pf.lineNumber}`)
-);
-const appliedSet = new Set(
- appliedFindings.map(af => `${af.file}:${af.lineNumber}`)
-);
-
-const hasHistory = previousFindings.length > 0;
-```
-
-
+
+
@@ -196,85 +43,29 @@ Format each finding as: REDUNDANT[index]: comment text
If none are obvious: "No obvious comments found."
-{pairsText}
+{reviewData.pairsText}
+
-```ts eval
-const redundantIndices = [];
-const indexPattern = /REDUNDANT\[(\d+)\]/g;
-let m;
-while ((m = indexPattern.exec(sampleResult)) !== null) {
- const idx = parseInt(m[1], 10);
- if (idx >= 0 && idx < pairs.length) redundantIndices.push(idx);
-}
-
-const allFindings = redundantIndices.map(i => pairs[i]);
-const pendingFindings = allFindings.filter(f =>
- !dismissedSet.has(`${f.file}:${f.lineNumber}`)
-);
-const hasFindings = pendingFindings.length > 0;
+
-const checklistItems = [];
+
-for (const af of appliedFindings) {
- checklistItems.push({
- status: "applied",
- file: af.file,
- lineNumber: af.lineNumber,
- label: "removed",
- });
-}
-
-for (const df of dismissedReplies) {
- checklistItems.push({
- status: "dismissed",
- file: df.file,
- lineNumber: df.lineNumber,
- comment: df.comment ?? "",
- label: df.replyText,
- });
-}
-
-for (const pf of pendingFindings) {
- checklistItems.push({
- status: "pending",
- file: pf.file,
- lineNumber: pf.lineNumber,
- comment: pf.comment,
- });
-}
-
-// Redeclared, not reassigned: bindings from earlier eval blocks arrive in
-// later blocks as consts, so assignment throws. A fresh declaration shadows
-// the injected binding and its export overrides env for the below.
-const hasChecklist = checklistItems.length > 0;
-const checklistMd = checklistItems.map(item => {
- const checked = item.status !== "pending" ? "x" : " ";
- if (item.status === "applied") {
- return `- [${checked}] \`${item.file}:${item.lineNumber}\` (removed)`;
- }
- if (item.status === "dismissed") {
- return `- [${checked}] \`${item.file}:${item.lineNumber}\` — \`${item.comment}\` (kept: "${item.label}")`;
- }
- return `- [${checked}] \`${item.file}:${item.lineNumber}\` — \`${item.comment}\``;
-}).join("\n");
-
-const newDismissReplies = dismissedReplies.filter(d => !d.alreadyProcessed);
-```
-
-
-
-
-
-
+
-
+
-{checklistMd}
+{state.checklistMd}
diff --git a/.reviews/components/CommentReviewData.ts b/.reviews/components/CommentReviewData.ts
new file mode 100644
index 00000000..a5316747
--- /dev/null
+++ b/.reviews/components/CommentReviewData.ts
@@ -0,0 +1,286 @@
+import type { Operation } from "effection";
+import { fetch } from "@effectionx/fetch";
+import { env as runtimeEnv } from "@executablemd/runtime";
+import type { PR } from "@executablemd/code-review-agent";
+
+export const props = {
+ type: "object",
+ properties: { pr: { type: "object" } },
+ required: ["pr"],
+ additionalProperties: false,
+};
+
+const locationProperties = {
+ file: { type: "string" },
+ lineNumber: { type: "number" },
+};
+
+const replyProperties = {
+ ...locationProperties,
+ comment: { type: "string" },
+ botCommentId: { type: "number" },
+ replyText: { type: "string" },
+ replyId: { type: "number" },
+ alreadyProcessed: { type: "boolean" },
+};
+
+export const returns = {
+ type: "object",
+ properties: {
+ pairs: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ comment: { type: "string" },
+ code: { type: "string" },
+ ...locationProperties,
+ },
+ required: ["comment", "code", "file", "lineNumber"],
+ additionalProperties: false,
+ },
+ },
+ hasPairs: { type: "boolean" },
+ pairsText: { type: "string" },
+ previousFindings: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: locationProperties,
+ required: ["file", "lineNumber"],
+ additionalProperties: false,
+ },
+ },
+ dismissedReplies: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: replyProperties,
+ required: ["file", "lineNumber", "replyText"],
+ additionalProperties: false,
+ },
+ },
+ repliesForClassification: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: replyProperties,
+ required: ["file", "lineNumber", "replyText"],
+ additionalProperties: false,
+ },
+ },
+ hasRepliesToClassify: { type: "boolean" },
+ repliesText: { type: "string" },
+ },
+ required: [
+ "pairs",
+ "hasPairs",
+ "pairsText",
+ "previousFindings",
+ "dismissedReplies",
+ "repliesForClassification",
+ "hasRepliesToClassify",
+ "repliesText",
+ ],
+ additionalProperties: false,
+};
+
+interface CommentReviewProps {
+ pr: PR;
+}
+
+interface Pair {
+ comment: string;
+ code: string;
+ file: string;
+ lineNumber: number;
+}
+
+interface Reply {
+ file: string;
+ lineNumber: number;
+ comment?: string;
+ botCommentId?: number;
+ replyText: string;
+ replyId?: number;
+ alreadyProcessed?: boolean;
+}
+
+interface Location {
+ file: string;
+ lineNumber: number;
+ comment: string;
+}
+
+interface ReviewData {
+ pairs: Pair[];
+ hasPairs: boolean;
+ pairsText: string;
+ previousFindings: Array<{ file: string; lineNumber: number }>;
+ dismissedReplies: Reply[];
+ repliesForClassification: Reply[];
+ hasRepliesToClassify: boolean;
+ repliesText: string;
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function stringValue(record: Record, key: string): string | undefined {
+ const value = record[key];
+ return typeof value === "string" ? value : undefined;
+}
+
+function numberValue(record: Record, key: string): number | undefined {
+ const value = record[key];
+ return typeof value === "number" ? value : undefined;
+}
+
+function records(value: unknown): Record[] {
+ return Array.isArray(value) ? value.filter(isRecord) : [];
+}
+
+function userLogin(record: Record): string | undefined {
+ const user = record.user;
+ return isRecord(user) ? stringValue(user, "login") : undefined;
+}
+
+function isBotReview(record: Record): boolean {
+ return (
+ userLogin(record) === "github-actions[bot]" &&
+ (stringValue(record, "body")?.includes("Redundant comment") ?? false)
+ );
+}
+
+function* githubApi(): Operation {
+ const token = yield* runtimeEnv("GITHUB_TOKEN");
+ const repository = yield* runtimeEnv("GITHUB_REPOSITORY");
+ const number = yield* runtimeEnv("PR_NUMBER");
+ if (!token || !repository || !number) {
+ return undefined;
+ }
+ const [owner, name] = repository.split("/");
+ return owner && name ? `https://api.github.com/repos/${owner}/${name}` : undefined;
+}
+
+function pairsFor(pr: PR): Pair[] {
+ const pairs: Pair[] = [];
+ const lines = pr.added.filter((line) => !line.isTest);
+ for (let i = 0; i < lines.length - 1; i++) {
+ const current = lines[i].content.trim();
+ const next = lines[i + 1].content.trim();
+ if (current.startsWith("//") && !next.startsWith("//") && next.length > 0) {
+ pairs.push({
+ comment: current,
+ code: next,
+ file: lines[i].file,
+ lineNumber: lines[i].lineNumber,
+ });
+ }
+ }
+ return pairs;
+}
+
+function* fetchReplyReactions(api: string, reply: Reply): Operation {
+ try {
+ const reactions = records(
+ yield* fetch(`${api}/pulls/comments/${reply.replyId}/reactions`).expect().json(),
+ );
+ return reactions.some(
+ (reaction) => userLogin(reaction) === "github-actions[bot]" && reaction.content === "+1",
+ );
+ } catch {
+ return false;
+ }
+}
+
+function* collectReplies(
+ api: string,
+ comments: Record[],
+ botCommentMap: Map,
+): Operation<{ dismissed: Reply[]; pending: Reply[] }> {
+ const dismissed: Reply[] = [];
+ const pending: Reply[] = [];
+ for (const reply of comments) {
+ const parentId = numberValue(reply, "in_reply_to_id");
+ const location = parentId === undefined ? undefined : botCommentMap.get(parentId);
+ if (!location || userLogin(reply) === "Bot") {
+ continue;
+ }
+ const replyText = stringValue(reply, "body");
+ const replyId = numberValue(reply, "id");
+ if (!replyText || replyId === undefined) {
+ continue;
+ }
+ const entry: Reply = { ...location, botCommentId: parentId, replyText, replyId };
+ if (yield* fetchReplyReactions(api, entry)) {
+ dismissed.push({ ...entry, alreadyProcessed: true });
+ } else {
+ pending.push(entry);
+ }
+ }
+ return { dismissed, pending };
+}
+
+export default function* CommentReviewData({ pr }: CommentReviewProps): Operation {
+ const pairs = pairsFor(pr);
+ const api = yield* githubApi();
+ const previousFindings: Array<{ file: string; lineNumber: number }> = [];
+ const dismissedReplies: Reply[] = [];
+ const repliesForClassification: Reply[] = [];
+
+ if (api) {
+ const number = yield* runtimeEnv("PR_NUMBER");
+ const comments = records(
+ yield* fetch(`${api}/pulls/${number}/comments?per_page=100`).expect().json(),
+ );
+ const botComments = comments.filter(isBotReview);
+ const botCommentMap = new Map();
+ for (const comment of botComments) {
+ const id = numberValue(comment, "id");
+ const path = stringValue(comment, "path");
+ const lineNumber = numberValue(comment, "original_line") ?? numberValue(comment, "line");
+ if (id === undefined || !path || lineNumber === undefined) {
+ continue;
+ }
+ const diffHunk = stringValue(comment, "diff_hunk") ?? "";
+ const commentLine =
+ diffHunk
+ .split("\n")
+ .filter((line) => line.startsWith("+"))
+ .pop() ?? "";
+ const location = {
+ file: path,
+ lineNumber,
+ comment: commentLine.replace(/^\+\s*/, "").trim(),
+ };
+ botCommentMap.set(id, location);
+ previousFindings.push({ file: path, lineNumber });
+ }
+ const replies = yield* collectReplies(api, comments, botCommentMap);
+ dismissedReplies.push(...replies.dismissed);
+ repliesForClassification.push(...replies.pending);
+ }
+
+ const hasPairs = pairs.length >= 3;
+ return {
+ pairs,
+ hasPairs,
+ pairsText: hasPairs
+ ? pairs
+ .map((pair, index) => `[${index}] COMMENT: ${pair.comment}\nCODE: ${pair.code}`)
+ .join("\n---\n")
+ : "",
+ previousFindings,
+ dismissedReplies,
+ repliesForClassification,
+ hasRepliesToClassify: repliesForClassification.length > 0,
+ repliesText: repliesForClassification
+ .map(
+ (reply, index) =>
+ `[${index}] FILE: ${reply.file}:${reply.lineNumber}\nREPLY: "${reply.replyText}"`,
+ )
+ .join("\n---\n"),
+ };
+}
diff --git a/.reviews/components/CommentReviewState.ts b/.reviews/components/CommentReviewState.ts
new file mode 100644
index 00000000..f24eb7fe
--- /dev/null
+++ b/.reviews/components/CommentReviewState.ts
@@ -0,0 +1,200 @@
+import type { Operation } from "effection";
+import type { PR } from "@executablemd/code-review-agent";
+
+const locationProperties = {
+ file: { type: "string" },
+ lineNumber: { type: "number" },
+};
+
+const findingProperties = {
+ ...locationProperties,
+ comment: { type: "string" },
+ code: { type: "string" },
+};
+
+const replyProperties = {
+ ...locationProperties,
+ comment: { type: "string" },
+ botCommentId: { type: "number" },
+ replyText: { type: "string" },
+ replyId: { type: "number" },
+ alreadyProcessed: { type: "boolean" },
+};
+
+export const props = {
+ type: "object",
+ properties: {
+ pr: { type: "object" },
+ data: { type: "object" },
+ classificationResult: { type: "string", default: "" },
+ sampleResult: { type: "string", default: "" },
+ },
+ required: ["pr", "data", "classificationResult", "sampleResult"],
+ additionalProperties: false,
+};
+
+export const returns = {
+ type: "object",
+ properties: {
+ hasChecklist: { type: "boolean" },
+ checklistMd: { type: "string" },
+ hasFindings: { type: "boolean" },
+ pendingFindings: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: findingProperties,
+ required: ["comment", "code", "file", "lineNumber"],
+ additionalProperties: false,
+ },
+ },
+ newDismissReplies: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: replyProperties,
+ required: ["file", "lineNumber", "replyText"],
+ additionalProperties: false,
+ },
+ },
+ },
+ required: ["hasChecklist", "checklistMd", "hasFindings", "pendingFindings", "newDismissReplies"],
+ additionalProperties: false,
+};
+
+interface Pair {
+ comment: string;
+ code: string;
+ file: string;
+ lineNumber: number;
+}
+
+interface Reply {
+ file: string;
+ lineNumber: number;
+ comment?: string;
+ botCommentId?: number;
+ replyText: string;
+ replyId?: number;
+ alreadyProcessed?: boolean;
+}
+
+interface ReviewData {
+ pairs: Pair[];
+ previousFindings: Array<{ file: string; lineNumber: number }>;
+ dismissedReplies: Reply[];
+ repliesForClassification: Reply[];
+}
+
+interface CommentReviewStateProps {
+ pr: PR;
+ data: ReviewData;
+ classificationResult: string;
+ sampleResult: string;
+}
+
+interface CommentReviewStateValue {
+ hasChecklist: boolean;
+ checklistMd: string;
+ hasFindings: boolean;
+ pendingFindings: Pair[];
+ newDismissReplies: Reply[];
+}
+
+type ChecklistItem =
+ | { status: "applied"; file: string; lineNumber: number; label: string }
+ | { status: "dismissed"; file: string; lineNumber: number; comment: string; label: string }
+ | { status: "pending"; file: string; lineNumber: number; comment: string };
+
+function key(file: string, lineNumber: number): string {
+ return `${file}:${lineNumber}`;
+}
+
+export default function* CommentReviewState({
+ pr,
+ data,
+ classificationResult,
+ sampleResult,
+}: CommentReviewStateProps): Operation {
+ const dismissedReplies = [...data.dismissedReplies];
+ const classificationPattern = /\[(\d+)\]\s*(DISMISS|ACCEPT)/gi;
+ let classificationMatch: RegExpExecArray | null;
+ while ((classificationMatch = classificationPattern.exec(classificationResult)) !== null) {
+ const index = Number.parseInt(classificationMatch[1], 10);
+ if (
+ index >= 0 &&
+ index < data.repliesForClassification.length &&
+ classificationMatch[2].toUpperCase() === "DISMISS"
+ ) {
+ dismissedReplies.push(data.repliesForClassification[index]);
+ }
+ }
+
+ const dismissedSet = new Set(dismissedReplies.map((reply) => key(reply.file, reply.lineNumber)));
+ const addedLineSet = new Set(pr.added.map((line) => key(line.file, line.lineNumber)));
+ const appliedFindings = data.previousFindings.filter(
+ (finding) =>
+ !addedLineSet.has(key(finding.file, finding.lineNumber)) &&
+ !dismissedSet.has(key(finding.file, finding.lineNumber)),
+ );
+ const redundantIndices: number[] = [];
+ const indexPattern = /REDUNDANT\[(\d+)\]/g;
+ let sampleMatch: RegExpExecArray | null;
+ while ((sampleMatch = indexPattern.exec(sampleResult)) !== null) {
+ const index = Number.parseInt(sampleMatch[1], 10);
+ if (index >= 0 && index < data.pairs.length) {
+ redundantIndices.push(index);
+ }
+ }
+ const pendingFindings = redundantIndices
+ .map((index) => data.pairs[index])
+ .filter((finding) => !dismissedSet.has(key(finding.file, finding.lineNumber)));
+
+ const checklistItems: ChecklistItem[] = [
+ ...appliedFindings.map(
+ (finding): ChecklistItem => ({
+ status: "applied",
+ file: finding.file,
+ lineNumber: finding.lineNumber,
+ label: "removed",
+ }),
+ ),
+ ...dismissedReplies.map(
+ (reply): ChecklistItem => ({
+ status: "dismissed",
+ file: reply.file,
+ lineNumber: reply.lineNumber,
+ comment: reply.comment ?? "",
+ label: reply.replyText,
+ }),
+ ),
+ ...pendingFindings.map(
+ (finding): ChecklistItem => ({
+ status: "pending",
+ file: finding.file,
+ lineNumber: finding.lineNumber,
+ comment: finding.comment,
+ }),
+ ),
+ ];
+ const checklistMd = checklistItems
+ .map((item) => {
+ const checked = item.status !== "pending" ? "x" : " ";
+ if (item.status === "applied") {
+ return `- [${checked}] \`${item.file}:${item.lineNumber}\` (removed)`;
+ }
+ if (item.status === "dismissed") {
+ return `- [${checked}] \`${item.file}:${item.lineNumber}\` — \`${item.comment}\` (kept: "${item.label}")`;
+ }
+ return `- [${checked}] \`${item.file}:${item.lineNumber}\` — \`${item.comment}\``;
+ })
+ .join("\n");
+
+ return {
+ hasChecklist: checklistItems.length > 0,
+ checklistMd,
+ hasFindings: pendingFindings.length > 0,
+ pendingFindings,
+ newDismissReplies: dismissedReplies.filter((reply) => !reply.alreadyProcessed),
+ };
+}
diff --git a/.reviews/components/DeepInfraProvider.md b/.reviews/components/DeepInfraProvider.md
index b0b474a7..c7433238 100644
--- a/.reviews/components/DeepInfraProvider.md
+++ b/.reviews/components/DeepInfraProvider.md
@@ -9,6 +9,8 @@ props:
---
```ts persist eval
+import { env as runtimeEnv } from "@executablemd/runtime";
+
yield* Sample.around({
*sample([context], next) {
if (context.model !== undefined && context.model !== props.model) {
@@ -20,19 +22,27 @@ yield* Sample.around({
messages.push({ role: "system", content: context.system });
}
messages.push({ role: "user", content: context.content });
+ const token = yield* runtimeEnv("DEEPINFRA_TOKEN");
+ if (!token) {
+ throw new Error("DeepInfraProvider requires DEEPINFRA_TOKEN");
+ }
const result = yield* fetch("https://api.deepinfra.com/v1/openai/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
- "Authorization": `Bearer ${process.env.DEEPINFRA_TOKEN}`,
+ "Authorization": `Bearer ${token}`,
},
body: JSON.stringify({ model: props.model, messages, temperature: 0, max_tokens: 4096 }),
})
.expect()
.json();
- return result.choices[0].message.content;
+ const content = result.choices?.[0]?.message?.content;
+ if (typeof content !== "string") {
+ throw new Error("DeepInfra response did not contain model content");
+ }
+ return content;
},
}, { at: 'min' });
```
diff --git a/.reviews/components/Doctor.md b/.reviews/components/Doctor.md
deleted file mode 100644
index b9fb0605..00000000
--- a/.reviews/components/Doctor.md
+++ /dev/null
@@ -1,239 +0,0 @@
----
-props:
- type: object
- properties:
- pr:
- type: object
- tsconfigPath:
- type: string
- default: ".reviews/tsconfig.oxlint.json"
- required: [pr]
- additionalProperties: false
----
-
-Checking environment for Oxlint static analysis...
-
-
-
-
-
-```bash exec
-.reviews/.oxlint/oxlint --version 2>/dev/null || echo "NOT_INSTALLED"
-```
-
-
-
-
-
-```bash exec
-test -x .reviews/.oxlint/tsgolint && echo "INSTALLED" || echo "NOT_INSTALLED"
-```
-
-
-
-
-
-```bash exec
-test -x .reviews/.oxlint/oxlint && echo "EXISTS" || echo "MISSING"
-```
-
-
-
-
-
-```bash exec
-test -f {props.tsconfigPath} && echo "EXISTS" || echo "MISSING"
-```
-
-
-
-```ts eval
-const oxlintInstalled = !oxlintVersion.includes("NOT_INSTALLED");
-const tsgolintInstalled = !tsgolintVersion.includes("NOT_INSTALLED");
-const nodeModulesExists = nodeModulesCheck.trim() === "EXISTS";
-const tsconfigExists = tsconfigCheck.trim() === "EXISTS";
-
-const canProbeTypeAware = oxlintInstalled && tsgolintInstalled
- && nodeModulesExists && tsconfigExists;
-```
-
-Scanning source files for scheme specifiers (jsr:, npm:)...
-
-
-
-```bash exec
-grep -rn --include='*.ts' --include='*.tsx' -E '^\s*(import|export)\s.*from\s+['"'"'"](jsr:|npm:)' packages/ src/ 2>/dev/null | head -50 || echo "NONE"
-```
-
-
-
-```ts eval
-const hasNativeSpecifiers = specifierScan.trim() !== "NONE"
- && specifierScan.trim().length > 0;
-
-const specifierLines = hasNativeSpecifiers
- ? specifierScan.trim().split("\n") : [];
-
-const specifierFiles = [...new Set(
- specifierLines.map(l => l.split(":")[0]).filter(Boolean)
-)];
-
-const jsrCount = specifierLines.filter(l => l.includes("jsr:")).length;
-const npmCount = specifierLines.filter(l => l.includes("npm:")).length;
-```
-
-Running type-aware probe to test Oxlint compatibility...
-
-
-
-
-
-```bash exec
-RESULT=$(OXLINT_TSGOLINT_PATH=.reviews/.oxlint/tsgolint .reviews/.oxlint/oxlint --config .reviews/.oxlintrc.json --type-aware --tsconfig {props.tsconfigPath} --format json 2>.reviews/probe-stderr.tmp || true)
-STDERR=$(cat .reviews/probe-stderr.tmp 2>/dev/null || echo "")
-rm -f .reviews/probe-stderr.tmp
-if [ -n "$RESULT" ] && printf '%s' "$RESULT" | jq -c --arg stderr "$STDERR" '
- def entries:
- if type == "array" then .
- elif (.diagnostics? | type) == "array" then .diagnostics
- else []
- end;
- def file:
- if (.file? | type) == "string" then .file
- elif (.filename? | type) == "string" then .filename
- else ""
- end;
- def rule:
- if (.ruleId? | type) == "string" then .ruleId
- elif (.code? | type) == "string" then .code
- else "unknown"
- end;
- def message:
- if (.message? | type) == "string" then .message else "" end;
- def import_noise:
- ((.message | ascii_downcase | contains("cannot find module"))
- or (.ruleId | ascii_downcase | contains("import")));
- def crashed($value):
- ($value | ascii_downcase) as $lower
- | ($lower | contains("tsgolint"))
- and (($lower | contains("panic"))
- or ($lower | contains("oom"))
- or ($lower | contains("fatal")));
- entries
- | map({file: file, ruleId: rule, message: message}) as $diagnostics
- | {
- diagnosticCount: ($diagnostics | length),
- importNoiseCount: ([$diagnostics[] | select(import_noise)] | length),
- filesAnalyzed: ([$diagnostics[].file | select(length > 0)] | unique | length),
- filesSkipped: ([$diagnostics[] | select(import_noise) | .file | select(length > 0)] | unique | length),
- importErrors: ([$diagnostics[] | select(import_noise)] | length),
- availableRuleIds: ([$diagnostics[].ruleId | select(length > 0)] | unique),
- tsgolintCrashed: crashed($stderr)
- }
-'; then
- :
-else
- jq -cn --arg stderr "$STDERR" '
- def crashed($value):
- ($value | ascii_downcase) as $lower
- | ($lower | contains("tsgolint"))
- and (($lower | contains("panic"))
- or ($lower | contains("oom"))
- or ($lower | contains("fatal")));
- {
- diagnosticCount: 0,
- importNoiseCount: 0,
- filesAnalyzed: 0,
- filesSkipped: 0,
- importErrors: 0,
- availableRuleIds: [],
- tsgolintCrashed: crashed($stderr)
- }
- '
-fi
-```
-
-
-
-
-
-```ts eval
-const BLOAT_RULES = [
- "no-unused-vars", "no-inferrable-types", "no-empty-function",
- "no-empty-object-type", "no-useless-empty-export",
- "no-unnecessary-type-constraint",
- "no-unnecessary-parameter-property-assignment",
- "no-static-only-class", "no-console", "no-debugger",
- "no-unnecessary-type-assertion", "no-redundant-type-constituents",
- "no-unnecessary-type-arguments",
- "no-unnecessary-boolean-literal-compare",
-];
-const TYPE_AWARE_RULES = [
- "no-unnecessary-type-assertion", "no-redundant-type-constituents",
- "no-unnecessary-type-arguments",
- "no-unnecessary-boolean-literal-compare",
-];
-
-let probe = {
- diagnosticCount: 0,
- importNoiseCount: 0,
- filesAnalyzed: 0,
- filesSkipped: 0,
- importErrors: 0,
- availableRuleIds: [],
- tsgolintCrashed: false,
-};
-try { probe = { ...probe, ...JSON.parse(probeResult) }; } catch { }
-
-const diagnosticCount = typeof probe.diagnosticCount === "number"
- ? probe.diagnosticCount : 0;
-const importNoiseCount = typeof probe.importNoiseCount === "number"
- ? probe.importNoiseCount : 0;
-const noiseRatio = diagnosticCount > 0
- ? importNoiseCount / diagnosticCount : 0;
-
-const tsgolintCrashed = probe.tsgolintCrashed === true;
-
-const typeAwareAvailable = canProbeTypeAware && !tsgolintCrashed;
-
-let recommendation = "syntax-only";
-if (typeAwareAvailable && noiseRatio < 0.3) {
- recommendation = "type-aware";
-} else if (typeAwareAvailable && noiseRatio >= 0.3) {
- recommendation = "type-aware-filtered";
-}
-
-const bloatRulesAvailable = typeAwareAvailable
- ? BLOAT_RULES
- : BLOAT_RULES.filter(r => !TYPE_AWARE_RULES.includes(r));
-const bloatRulesMissing = typeAwareAvailable
- ? []
- : TYPE_AWARE_RULES;
-
-const doctor = {
- oxlintInstalled,
- oxlintVersion: oxlintVersion.trim(),
- tsgolintInstalled,
- tsgolintVersion: tsgolintVersion.trim(),
- tsconfigExists,
- nodeModulesExists,
- typeAwareAvailable,
- filesAnalyzed: typeof probe.filesAnalyzed === "number" ? probe.filesAnalyzed : 0,
- filesSkipped: typeof probe.filesSkipped === "number" ? probe.filesSkipped : 0,
- importErrors: typeof probe.importErrors === "number" ? probe.importErrors : 0,
- availableRuleIds: Array.isArray(probe.availableRuleIds)
- ? probe.availableRuleIds : [],
- bloatRulesAvailable,
- bloatRulesMissing,
- recommendation,
- nativeSpecifiers: {
- count: hasNativeSpecifiers ? specifierLines.length : 0,
- files: specifierFiles,
- jsr: jsrCount,
- npm: npmCount,
- },
-};
-
-return '```json\n' + JSON.stringify(doctor) + '\n```';
-```
diff --git a/.reviews/components/Doctor.ts b/.reviews/components/Doctor.ts
new file mode 100644
index 00000000..68a79613
--- /dev/null
+++ b/.reviews/components/Doctor.ts
@@ -0,0 +1,171 @@
+import type { Operation } from "effection";
+import {
+ buildDoctorResult,
+ isOxlintCrash,
+ normalizeOxlintOutput,
+ summarizeDoctorProbe,
+} from "@executablemd/code-review-agent";
+import type { DoctorResult, OxlintDiagnostic } from "@executablemd/code-review-agent";
+import { exec, glob, readTextFile, stat } from "@executablemd/runtime";
+
+export const props = {
+ type: "object",
+ properties: {
+ pr: { type: "object" },
+ tsconfigPath: {
+ type: "string",
+ default: ".reviews/tsconfig.oxlint.json",
+ },
+ },
+ required: ["pr"],
+ additionalProperties: false,
+};
+
+export const returns = {
+ type: "object",
+ properties: {
+ oxlintInstalled: { type: "boolean" },
+ oxlintVersion: { type: "string" },
+ tsgolintInstalled: { type: "boolean" },
+ tsgolintVersion: { type: "string" },
+ tsconfigExists: { type: "boolean" },
+ nodeModulesExists: { type: "boolean" },
+ typeAwareAvailable: { type: "boolean" },
+ filesAnalyzed: { type: "number" },
+ filesSkipped: { type: "number" },
+ importErrors: { type: "number" },
+ bloatRulesAvailable: { type: "array", items: { type: "string" } },
+ bloatRulesMissing: { type: "array", items: { type: "string" } },
+ recommendation: { type: "string" },
+ nativeSpecifiers: {
+ type: "object",
+ properties: {
+ count: { type: "number" },
+ files: { type: "array", items: { type: "string" } },
+ jsr: { type: "number" },
+ npm: { type: "number" },
+ },
+ required: ["count", "files", "jsr", "npm"],
+ additionalProperties: false,
+ },
+ },
+ required: [
+ "oxlintInstalled",
+ "oxlintVersion",
+ "tsgolintInstalled",
+ "tsgolintVersion",
+ "tsconfigExists",
+ "nodeModulesExists",
+ "typeAwareAvailable",
+ "filesAnalyzed",
+ "filesSkipped",
+ "importErrors",
+ "bloatRulesAvailable",
+ "bloatRulesMissing",
+ "recommendation",
+ "nativeSpecifiers",
+ ],
+ additionalProperties: false,
+};
+
+interface DoctorProps {
+ pr: object;
+ tsconfigPath?: string;
+}
+
+function* version(command: string[]): Operation {
+ const result = yield* exec({ command });
+ return result.exitCode === 0 ? result.stdout.trim() : "";
+}
+
+function* nativeSpecifierSummary(): Operation {
+ const entries = yield* glob({
+ root: ".",
+ patterns: ["packages/**/*.ts", "durable-effects/**/*.ts"],
+ exclude: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"],
+ });
+ const files: string[] = [];
+ let jsr = 0;
+ let npm = 0;
+ for (const entry of entries) {
+ if (!entry.isFile) {
+ continue;
+ }
+ const source = yield* readTextFile(entry.path);
+ for (const line of source.split(/\r?\n/)) {
+ const match = /^\s*(?:import|export)\s.*?from\s+["'](jsr:|npm:)/.exec(line);
+ if (!match) {
+ continue;
+ }
+ files.push(entry.path);
+ if (match[1] === "jsr:") {
+ jsr++;
+ } else {
+ npm++;
+ }
+ }
+ }
+ return { count: jsr + npm, files: [...new Set(files)], jsr, npm };
+}
+
+function* probeTypeAware(canProbe: boolean, tsconfigPath: string) {
+ if (!canProbe) {
+ return summarizeDoctorProbe({ diagnostics: [], stderr: "", exitCode: 2 });
+ }
+
+ const result = yield* exec({
+ command: [
+ ".reviews/.oxlint/oxlint",
+ "--config",
+ ".reviews/.oxlintrc.json",
+ "--type-aware",
+ "--tsconfig",
+ tsconfigPath,
+ "--format",
+ "json",
+ ],
+ env: { OXLINT_TSGOLINT_PATH: ".reviews/.oxlint/tsgolint" },
+ });
+ if (result.exitCode > 1 && !isOxlintCrash(result.stderr)) {
+ throw new Error(result.stderr || `Oxlint probe failed with exit code ${result.exitCode}`);
+ }
+ if (result.stdout.trim().length === 0 && result.exitCode !== 0 && !isOxlintCrash(result.stderr)) {
+ throw new Error(result.stderr || "Oxlint probe returned no JSON output");
+ }
+ const diagnostics: OxlintDiagnostic[] =
+ result.stdout.trim().length === 0 ? [] : normalizeOxlintOutput(result.stdout);
+ return summarizeDoctorProbe({
+ diagnostics,
+ stderr: result.stderr,
+ exitCode: result.exitCode,
+ });
+}
+
+export default function* Doctor({
+ tsconfigPath = ".reviews/tsconfig.oxlint.json",
+}: DoctorProps): Operation {
+ const oxlint = yield* stat(".reviews/.oxlint/oxlint");
+ const tsgolint = yield* stat(".reviews/.oxlint/tsgolint");
+ const tsconfig = yield* stat(tsconfigPath);
+ const nodeModules = yield* stat("node_modules");
+ const oxlintVersion = oxlint.isFile
+ ? yield* version([".reviews/.oxlint/oxlint", "--version"])
+ : "";
+ const tsgolintVersion = tsgolint.isFile
+ ? yield* version([".reviews/.oxlint/tsgolint", "--version"])
+ : "";
+ const canProbe = oxlint.isFile && tsgolint.isFile && tsconfig.isFile && nodeModules.isDirectory;
+ const probe = yield* probeTypeAware(canProbe, tsconfigPath);
+ return buildDoctorResult(
+ {
+ oxlintInstalled: oxlint.isFile,
+ oxlintVersion,
+ tsgolintInstalled: tsgolint.isFile,
+ tsgolintVersion,
+ tsconfigExists: tsconfig.isFile,
+ nodeModulesExists: nodeModules.isDirectory,
+ nativeSpecifiers: yield* nativeSpecifierSummary(),
+ },
+ probe,
+ );
+}
diff --git a/.reviews/components/EnsureOxlint.md b/.reviews/components/EnsureOxlint.md
index 09b67771..56cad2f8 100644
--- a/.reviews/components/EnsureOxlint.md
+++ b/.reviews/components/EnsureOxlint.md
@@ -8,70 +8,84 @@ props:
additionalProperties: false
---
-```bash silent exec
-# Provisions the pinned oxlint + tsgolint binaries with mandatory sha256
-# verification, failing closed — a failed or tampered download must abort the
-# review, never silently degrade it. Version pins live next to their hashes so
-# neither can change without the other. The workflow guards on the resulting
-# binaries (see review.yml).
-set -euo pipefail
-DIR="{props.dir}"
-OXLINT_TAG="apps_v1.74.0"
-TSGOLINT_VERSION="0.25.0"
-mkdir -p "$DIR"
+```ts eval
+import { platform } from "@executablemd/runtime";
-os="$(uname -s)"
-arch="$(uname -m)"
-case "$os" in
- Darwin) ox_os="apple-darwin"; tg_os="darwin" ;;
- Linux) ox_os="unknown-linux-gnu"; tg_os="linux" ;;
- *) echo "EnsureOxlint: unsupported OS $os" >&2; exit 1 ;;
-esac
-case "$arch" in
- arm64 | aarch64) ox_arch="aarch64"; tg_arch="arm64" ;;
- x86_64 | amd64) ox_arch="x86_64"; tg_arch="x64" ;;
- *) echo "EnsureOxlint: unsupported arch $arch" >&2; exit 1 ;;
-esac
+const OXLINT_TAG = "apps_v1.74.0";
+const TSGOLINT_VERSION = "0.25.0";
+const host = yield* platform();
+const target = {
+ oxlintOs: host.os === "darwin" ? "apple-darwin" : host.os === "linux" ? "unknown-linux-gnu" : "",
+ tsgolintOs: host.os === "darwin" ? "darwin" : host.os === "linux" ? "linux" : "",
+ oxlintArch: host.arch === "arm64" || host.arch === "aarch64"
+ ? "aarch64"
+ : host.arch === "x86_64" || host.arch === "amd64" || host.arch === "x64"
+ ? "x86_64"
+ : "",
+ tsgolintArch: host.arch === "arm64" || host.arch === "aarch64"
+ ? "arm64"
+ : host.arch === "x86_64" || host.arch === "amd64" || host.arch === "x64"
+ ? "x64"
+ : "",
+};
+if (!target.oxlintOs || !target.oxlintArch) {
+ throw new Error(`EnsureOxlint: unsupported platform ${host.os}/${host.arch}`);
+}
-case "${ox_arch}-${ox_os}" in
- aarch64-apple-darwin) ox_sha="768a2d00e7e0a95cbf89837086f475d25dc1a1ba605b8831fb5a1db6d590a643" ;;
- x86_64-apple-darwin) ox_sha="04ae38d56ae4990ac96320c03f05f38cf1103b5fe64b08cd7203b87e767b45b4" ;;
- aarch64-unknown-linux-gnu) ox_sha="ced0d2433bda2b4295e1ab93b40c3f24224713c32a44e13abcda656590dba1cb" ;;
- x86_64-unknown-linux-gnu) ox_sha="fd3ed5d2dc55ab6f7a243583c69dd5da4ac97cd1f6e10321225ca6343c9451a9" ;;
-esac
-case "${tg_os}-${tg_arch}" in
- darwin-arm64) tg_sha="3ad51d1b88070b491b81a4f5c6169148914127b9da65f8087823b25568431e1e" ;;
- darwin-x64) tg_sha="6b78caa20db383c055cead96724aeafab3cd277b00b1f95c6f56b4bfcf22fd60" ;;
- linux-arm64) tg_sha="20bcbab4bb37dd396102566740ee39e67d1e3d16d06096802180426c022bc414" ;;
- linux-x64) tg_sha="f6ea083842395d7439eadbbbf380f23793a1fa890fbda92013ee0f6033e75630" ;;
-esac
+const oxlintHashes = {
+ "aarch64-apple-darwin": "768a2d00e7e0a95cbf89837086f475d25dc1a1ba605b8831fb5a1db6d590a643",
+ "x86_64-apple-darwin": "04ae38d56ae4990ac96320c03f05f38cf1103b5fe64b08cd7203b87e767b45b4",
+ "aarch64-unknown-linux-gnu": "ced0d2433bda2b4295e1ab93b40c3f24224713c32a44e13abcda656590dba1cb",
+ "x86_64-unknown-linux-gnu": "fd3ed5d2dc55ab6f7a243583c69dd5da4ac97cd1f6e10321225ca6343c9451a9",
+};
+const tsgolintHashes = {
+ "darwin-arm64": "3ad51d1b88070b491b81a4f5c6169148914127b9da65f8087823b25568431e1e",
+ "darwin-x64": "6b78caa20db383c055cead96724aeafab3cd277b00b1f95c6f56b4bfcf22fd60",
+ "linux-arm64": "20bcbab4bb37dd396102566740ee39e67d1e3d16d06096802180426c022bc414",
+ "linux-x64": "f6ea083842395d7439eadbbbf380f23793a1fa890fbda92013ee0f6033e75630",
+};
+const oxlintKey = `${target.oxlintArch}-${target.oxlintOs}`;
+const tsgolintKey = `${target.tsgolintOs}-${target.tsgolintArch}`;
+const oxlintUrl = `https://github.com/oxc-project/oxc/releases/download/${OXLINT_TAG}/oxlint-${target.oxlintArch}-${target.oxlintOs}.tar.gz`;
+const tsgolintUrl = `https://registry.npmjs.org/@oxlint-tsgolint/${target.tsgolintOs}-${target.tsgolintArch}/-/${target.tsgolintOs}-${target.tsgolintArch}-${TSGOLINT_VERSION}.tgz`;
+const oxlintSha = oxlintHashes[oxlintKey];
+const tsgolintSha = tsgolintHashes[tsgolintKey];
+if (!oxlintSha || !tsgolintSha) {
+ throw new Error(`EnsureOxlint: no checksum for ${oxlintKey}/${tsgolintKey}`);
+}
+```
-if command -v sha256sum >/dev/null 2>&1; then sha_cmd="sha256sum"; else sha_cmd="shasum -a 256"; fi
+```bash silent exec
+set -euo pipefail
+DIR="{dir}"
+mkdir -p "$DIR"
verify() {
- actual="$($sha_cmd "$1" | awk '{print $1}')"
- if [ "$actual" != "$2" ]; then
- echo "EnsureOxlint: checksum mismatch for $1 (expected $2, got $actual)" >&2
+ expected="$2"
+ actual="$(sha256sum "$1" 2>/dev/null || shasum -a 256 "$1")"
+ actual="${actual%% *}"
+ if [ "$actual" != "$expected" ]; then
+ echo "EnsureOxlint: checksum mismatch for $1" >&2
exit 1
fi
}
-# oxlint: standalone binary from the oxc release (no Node/npm needed).
if [ ! -x "$DIR/oxlint" ]; then
tmp="$(mktemp -d)"
- curl -fsSL -o "$tmp/oxlint.tar.gz" "https://github.com/oxc-project/oxc/releases/download/${OXLINT_TAG}/oxlint-${ox_arch}-${ox_os}.tar.gz"
- verify "$tmp/oxlint.tar.gz" "$ox_sha"
+ trap 'rm -rf "$tmp"' EXIT
+ curl -fsSL -o "$tmp/oxlint.tar.gz" "{oxlintUrl}"
+ verify "$tmp/oxlint.tar.gz" "{oxlintSha}"
tar xz -C "$tmp" -f "$tmp/oxlint.tar.gz"
- mv "$tmp/oxlint-${ox_arch}-${ox_os}" "$DIR/oxlint"
+ mv "$tmp/oxlint-{target.oxlintArch}-{target.oxlintOs}" "$DIR/oxlint"
chmod +x "$DIR/oxlint"
rm -rf "$tmp"
fi
-# tsgolint (type-aware engine): npm-only, so pull the platform tarball directly.
if [ ! -x "$DIR/tsgolint" ]; then
tmp="$(mktemp -d)"
- curl -fsSL -o "$tmp/tsgolint.tgz" "https://registry.npmjs.org/@oxlint-tsgolint/${tg_os}-${tg_arch}/-/${tg_os}-${tg_arch}-${TSGOLINT_VERSION}.tgz"
- verify "$tmp/tsgolint.tgz" "$tg_sha"
+ trap 'rm -rf "$tmp"' EXIT
+ curl -fsSL -o "$tmp/tsgolint.tgz" "{tsgolintUrl}"
+ verify "$tmp/tsgolint.tgz" "{tsgolintSha}"
tar xz -C "$tmp" -f "$tmp/tsgolint.tgz"
mv "$tmp/package/tsgolint" "$DIR/tsgolint"
chmod +x "$DIR/tsgolint"
diff --git a/.reviews/components/GitHubAuth.md b/.reviews/components/GitHubAuth.md
new file mode 100644
index 00000000..5affdd12
--- /dev/null
+++ b/.reviews/components/GitHubAuth.md
@@ -0,0 +1,46 @@
+---
+props:
+ type: object
+ properties: {}
+ additionalProperties: false
+---
+
+```ts persist eval
+import { FetchApi } from "@effectionx/fetch";
+import { env as runtimeEnv } from "@executablemd/runtime";
+
+function* installGitHubAuth() {
+ const token = yield* runtimeEnv("GITHUB_TOKEN");
+ if (!token) {
+ return;
+ }
+ yield* FetchApi.around({
+ *fetch([input, init, shouldExpect], next) {
+ let url;
+ try {
+ url = input instanceof Request ? new URL(input.url) : new URL(input);
+ } catch {
+ return yield* next(input, init, shouldExpect);
+ }
+
+ if (url.protocol !== "https:" || url.hostname !== "api.github.com") {
+ return yield* next(input, init, shouldExpect);
+ }
+
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
+ const requestHeaders = new Headers(init?.headers);
+ requestHeaders.forEach((value, key) => headers.set(key, value));
+ headers.set("Authorization", `Bearer ${token}`);
+ if (!headers.has("Accept")) {
+ headers.set("Accept", "application/vnd.github+json");
+ }
+
+ return yield* next(input, { ...init, headers }, shouldExpect);
+ },
+ });
+}
+
+yield* installGitHubAuth();
+```
+
+
diff --git a/.reviews/components/GitHubComment.md b/.reviews/components/GitHubComment.md
index 3d7ee32f..30a6f03b 100644
--- a/.reviews/components/GitHubComment.md
+++ b/.reviews/components/GitHubComment.md
@@ -9,47 +9,39 @@ props:
---
```ts eval
-const content = yield* renderChildren();
-const body = props.marker + "\n" + content.trim();
+import { env as runtimeEnv } from "@executablemd/runtime";
-const repo = process.env.GITHUB_REPOSITORY;
-const prNumber = process.env.PR_NUMBER;
-const [owner, name] = repo.split("/");
-const api = `https://api.github.com/repos/${owner}/${name}`;
-
-function githubHeaders() {
- return {
- "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`,
- "Accept": "application/vnd.github+json",
- };
+function* reviewConfiguration() {
+ const repository = yield* runtimeEnv("GITHUB_REPOSITORY");
+ const number = yield* runtimeEnv("PR_NUMBER");
+ if (!repository || !number || !repository.includes("/")) {
+ throw new Error("GitHubComment requires GITHUB_REPOSITORY and PR_NUMBER");
+ }
+ const [owner, name] = repository.split("/");
+ return { api: `https://api.github.com/repos/${owner}/${name}`, number };
}
-const commentsResult = yield* fetch(`${api}/issues/${prNumber}/comments`, {
- headers: githubHeaders(),
-})
- .expect()
- .json();
+const content = yield* renderChildren();
+const body = props.marker + "\n" + content.trim();
+const { api, number } = yield* reviewConfiguration();
+const comments = yield* fetch(`${api}/issues/${number}/comments`).expect().json();
+if (!Array.isArray(comments)) {
+ throw new Error("GitHub comments response was not an array");
+}
-const existing = commentsResult.find(c =>
- c.user.type === "Bot" && c.body.includes(props.marker)
+const existing = comments.find((comment) =>
+ comment.user?.type === "Bot" && typeof comment.body === "string" && comment.body.includes(props.marker)
);
-
if (existing) {
yield* fetch(`${api}/issues/comments/${existing.id}`, {
method: "PATCH",
- headers: {
- ...githubHeaders(),
- "Content-Type": "application/json",
- },
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body }),
}).expect();
} else {
- yield* fetch(`${api}/issues/${prNumber}/comments`, {
+ yield* fetch(`${api}/issues/${number}/comments`, {
method: "POST",
- headers: {
- ...githubHeaders(),
- "Content-Type": "application/json",
- },
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body }),
}).expect();
}
diff --git a/.reviews/components/OllamaProvider.md b/.reviews/components/OllamaProvider.md
index 69e24eab..f4573423 100644
--- a/.reviews/components/OllamaProvider.md
+++ b/.reviews/components/OllamaProvider.md
@@ -32,7 +32,11 @@ yield* Sample.around({
.expect()
.json();
- return result.choices[0].message.content;
+ const content = result.choices?.[0]?.message?.content;
+ if (typeof content !== "string") {
+ throw new Error("Ollama response did not contain model content");
+ }
+ return content;
},
}, { at: 'min' });
```
diff --git a/.reviews/components/OxlintConfig.md b/.reviews/components/OxlintConfig.md
new file mode 100644
index 00000000..b86d9994
--- /dev/null
+++ b/.reviews/components/OxlintConfig.md
@@ -0,0 +1,34 @@
+---
+props:
+ type: object
+ properties:
+ path:
+ type: string
+ default: ".reviews/tsconfig.oxlint.json"
+ additionalProperties: false
+---
+
+```ts eval
+import { ensureDir, writeTextFile } from "@executablemd/runtime";
+
+yield* ensureDir(".reviews");
+yield* writeTextFile(path, JSON.stringify({
+ compilerOptions: {
+ target: "ESNext",
+ module: "ESNext",
+ moduleResolution: "bundler",
+ strict: true,
+ noEmit: true,
+ skipLibCheck: true,
+ resolveJsonModule: true,
+ lib: ["ESNext", "DOM"],
+ types: [],
+ },
+ include: [
+ "packages/*/src/**/*.ts",
+ "packages/*/*.ts",
+ "durable-effects/**/*.ts",
+ ],
+ exclude: ["node_modules", "dist", ".vendor", "**/*.test.ts"],
+}, null, 2));
+```
diff --git a/.reviews/components/OxlintDiagnostics.ts b/.reviews/components/OxlintDiagnostics.ts
new file mode 100644
index 00000000..bdeb153b
--- /dev/null
+++ b/.reviews/components/OxlintDiagnostics.ts
@@ -0,0 +1,74 @@
+import type { Operation } from "effection";
+import { normalizeOxlintOutput } from "@executablemd/code-review-agent";
+import type { OxlintDiagnostic } from "@executablemd/code-review-agent";
+import { exec } from "@executablemd/runtime";
+
+export const props = {
+ type: "object",
+ properties: {
+ files: { type: "array", items: { type: "string" } },
+ typeAware: { type: "boolean", default: false },
+ tsconfigPath: { type: "string", default: ".reviews/tsconfig.oxlint.json" },
+ },
+ required: ["files"],
+ additionalProperties: false,
+};
+
+export const returns = {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ message: { type: "string" },
+ ruleId: { type: "string" },
+ severity: { type: "string" },
+ file: { type: "string" },
+ line: { type: "number" },
+ column: { type: "number" },
+ },
+ required: ["message", "ruleId", "severity", "file", "line", "column"],
+ additionalProperties: false,
+ },
+};
+
+interface OxlintDiagnosticsProps {
+ files: string[];
+ typeAware?: boolean;
+ tsconfigPath?: string;
+}
+
+export default function* OxlintDiagnostics({
+ files,
+ typeAware = false,
+ tsconfigPath = ".reviews/tsconfig.oxlint.json",
+}: OxlintDiagnosticsProps): Operation {
+ if (files.length === 0) {
+ return [];
+ }
+
+ const command = [
+ ".reviews/.oxlint/oxlint",
+ "--config",
+ ".reviews/.oxlintrc.json",
+ "--format",
+ "json",
+ ];
+ const environment: Record = {};
+ if (typeAware) {
+ command.push("--type-aware", "--tsconfig", tsconfigPath);
+ environment.OXLINT_TSGOLINT_PATH = ".reviews/.oxlint/tsgolint";
+ }
+ command.push(...files);
+
+ const result = yield* exec({ command, env: environment });
+ if (result.exitCode > 1) {
+ throw new Error(result.stderr || `Oxlint failed with exit code ${result.exitCode}`);
+ }
+ if (/panic|oom|out of memory|fatal|segmentation fault/i.test(result.stderr)) {
+ throw new Error(result.stderr);
+ }
+ if (result.stdout.trim().length === 0) {
+ throw new Error(result.stderr || "Oxlint returned no JSON output");
+ }
+ return normalizeOxlintOutput(result.stdout, files);
+}
diff --git a/.reviews/components/RepositoryInventory.ts b/.reviews/components/RepositoryInventory.ts
new file mode 100644
index 00000000..0f3af7e1
--- /dev/null
+++ b/.reviews/components/RepositoryInventory.ts
@@ -0,0 +1,48 @@
+import type { Operation } from "effection";
+import { glob, readTextFile } from "@executablemd/runtime";
+
+export const props = {
+ type: "object",
+ properties: {},
+ additionalProperties: false,
+};
+
+export const returns = {
+ type: "object",
+ properties: {
+ fileList: { type: "array", items: { type: "string" } },
+ fileCount: { type: "number" },
+ lineCount: { type: "number" },
+ },
+ required: ["fileList", "fileCount", "lineCount"],
+ additionalProperties: false,
+};
+
+interface RepositoryInventoryValue {
+ fileList: string[];
+ fileCount: number;
+ lineCount: number;
+}
+
+function lineCount(source: string): number {
+ return source.length === 0 ? 0 : source.split(/\r?\n/).length - (source.endsWith("\n") ? 1 : 0);
+}
+
+export default function* RepositoryInventory(
+ _props: Record,
+): Operation {
+ const entries = yield* glob({
+ root: ".",
+ patterns: ["durable-effects/**/*.ts", "packages/**/*.ts"],
+ exclude: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"],
+ });
+ const fileList = entries
+ .filter((entry) => entry.isFile)
+ .map((entry) => entry.path)
+ .sort();
+ let totalLines = 0;
+ for (const path of fileList) {
+ totalLines += lineCount(yield* readTextFile(path));
+ }
+ return { fileList, fileCount: fileList.length, lineCount: totalLines };
+}
diff --git a/.reviews/components/ReviewContext.ts b/.reviews/components/ReviewContext.ts
new file mode 100644
index 00000000..80ad6701
--- /dev/null
+++ b/.reviews/components/ReviewContext.ts
@@ -0,0 +1,78 @@
+import type { Operation } from "effection";
+import { fetch } from "@effectionx/fetch";
+import { parseDiff } from "@executablemd/code-review-agent";
+import type { PR } from "@executablemd/code-review-agent";
+import { env as runtimeEnv, exec } from "@executablemd/runtime";
+
+export const props = {
+ type: "object",
+ properties: {},
+ additionalProperties: false,
+};
+
+export const returns = {
+ type: "object",
+ properties: {
+ pr: { type: "object" },
+ changedFilePaths: { type: "array", items: { type: "string" } },
+ },
+ required: ["pr", "changedFilePaths"],
+ additionalProperties: false,
+};
+
+interface PullRequestResponse {
+ body?: unknown;
+}
+
+interface ReviewContextValue {
+ pr: Omit & { directories: string[] };
+ changedFilePaths: string[];
+}
+
+function isPullRequestResponse(value: unknown): value is PullRequestResponse {
+ return typeof value === "object" && value !== null;
+}
+
+function* pullBody(repository: string, number: string, fallback: string): Operation {
+ if (fallback || !repository || !number) {
+ return fallback;
+ }
+
+ const payload: unknown = yield* fetch(
+ `https://api.github.com/repos/${repository}/pulls/${number}`,
+ )
+ .expect()
+ .json();
+ if (!isPullRequestResponse(payload)) {
+ return fallback;
+ }
+ return typeof payload.body === "string" ? payload.body : fallback;
+}
+
+export default function* ReviewContext(
+ _props: Record,
+): Operation {
+ const base = (yield* runtimeEnv("BASE_SHA")) ?? "HEAD~1";
+ const head = (yield* runtimeEnv("HEAD_SHA")) ?? "HEAD";
+ const title = (yield* runtimeEnv("PR_TITLE")) ?? "";
+ const number = (yield* runtimeEnv("PR_NUMBER")) ?? "";
+ const repository = (yield* runtimeEnv("GITHUB_REPOSITORY")) ?? "";
+ const localBody = (yield* runtimeEnv("PR_BODY")) ?? "";
+ const range = `${base}...${head}`;
+
+ const diff = yield* exec({ command: ["git", "diff", range] });
+ if (diff.exitCode !== 0) {
+ throw new Error(diff.stderr || `git diff failed with exit code ${diff.exitCode}`);
+ }
+ const names = yield* exec({ command: ["git", "diff", "--name-status", range] });
+ if (names.exitCode !== 0) {
+ throw new Error(
+ names.stderr || `git diff --name-status failed with exit code ${names.exitCode}`,
+ );
+ }
+
+ const body = yield* pullBody(repository, number, localBody);
+ const parsed = parseDiff(diff.stdout, names.stdout, { title, body, number });
+ const pr = { ...parsed, directories: [...parsed.directories] };
+ return { pr, changedFilePaths: pr.files.map((file) => file.path) };
+}
diff --git a/.reviews/components/ReviewSetup.md b/.reviews/components/ReviewSetup.md
new file mode 100644
index 00000000..3b2bcd31
--- /dev/null
+++ b/.reviews/components/ReviewSetup.md
@@ -0,0 +1,30 @@
+---
+props:
+ type: object
+ properties:
+ pullModel:
+ type: boolean
+ default: false
+ model:
+ type: string
+ default: "qwen3:30b-a3b"
+ additionalProperties: false
+---
+
+
+
+
+
+```ts eval
+import { exec } from "@executablemd/runtime";
+
+if (pullModel) {
+ const installed = yield* exec({ command: ["ollama", "show", model] });
+ if (installed.exitCode !== 0) {
+ const pulled = yield* exec({ command: ["ollama", "pull", model] });
+ if (pulled.exitCode !== 0) {
+ throw new Error(pulled.stderr || `Unable to provision Ollama model ${model}`);
+ }
+ }
+}
+```
diff --git a/.reviews/components/SuggestRemoval.md b/.reviews/components/SuggestRemoval.md
index 82b31ef9..895de97b 100644
--- a/.reviews/components/SuggestRemoval.md
+++ b/.reviews/components/SuggestRemoval.md
@@ -12,28 +12,36 @@ props:
---
```ts eval
-const repo = process.env.GITHUB_REPOSITORY;
-const prNumber = process.env.PR_NUMBER;
-const headSha = process.env.HEAD_SHA;
+import { env as runtimeEnv } from "@executablemd/runtime";
-function githubHeaders() {
+function* githubConfiguration() {
+ const token = yield* runtimeEnv("GITHUB_TOKEN");
+ const repo = yield* runtimeEnv("GITHUB_REPOSITORY");
+ const prNumber = yield* runtimeEnv("PR_NUMBER");
+ const headSha = yield* runtimeEnv("HEAD_SHA");
+ if (!token || !repo || !prNumber || !headSha) {
+ return undefined;
+ }
+ const [owner, name] = repo.split("/");
return {
- "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`,
- "Accept": "application/vnd.github+json",
- "Content-Type": "application/json",
+ api: `https://api.github.com/repos/${owner}/${name}`,
+ graphql: "https://api.github.com/graphql",
+ prNumber,
+ headSha,
+ owner,
+ name,
};
}
-if (!process.env.GITHUB_TOKEN || !repo || !prNumber || !headSha) {
+const github = yield* githubConfiguration();
+if (!github) {
return "";
}
-const [owner, name] = repo.split("/");
-const api = `https://api.github.com/repos/${owner}/${name}`;
-const graphql = "https://api.github.com/graphql";
+const { api, graphql, prNumber, headSha, owner, name } = github;
const existingReviews = yield* fetch(
- `${api}/pulls/${prNumber}/reviews`, { headers: githubHeaders() }
+ `${api}/pulls/${prNumber}/reviews`
).expect().json();
const botReviews = existingReviews.filter(r =>
@@ -45,7 +53,6 @@ for (const review of botReviews) {
try {
yield* fetch(`${api}/pulls/${prNumber}/reviews/${review.id}`, {
method: "DELETE",
- headers: githubHeaders(),
}).expect();
} catch {
// Review may already be submitted (can't delete submitted reviews).
@@ -75,7 +82,7 @@ if (props.dismissedReplies.length > 0) {
try {
const threadsResult = yield* fetch(graphql, {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: threadsQuery,
variables: { owner, name, pr: parseInt(prNumber, 10) },
@@ -99,7 +106,7 @@ if (props.dismissedReplies.length > 0) {
try {
yield* fetch(`${api}/pulls/comments/${reply.replyId}/reactions`, {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: "+1" }),
}).expect();
} catch {}
@@ -112,7 +119,7 @@ if (props.dismissedReplies.length > 0) {
try {
yield* fetch(graphql, {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `mutation($threadId: ID!) {
resolveReviewThread(input: { threadId: $threadId }) {
@@ -138,7 +145,7 @@ if (props.findings.length > 0) {
yield* fetch(`${api}/pulls/${prNumber}/reviews`, {
method: "POST",
- headers: githubHeaders(),
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
commit_id: headSha,
event: "COMMENT",
diff --git a/.reviews/components/Threshold.md b/.reviews/components/Threshold.md
index 14738b9d..438e65f1 100644
--- a/.reviews/components/Threshold.md
+++ b/.reviews/components/Threshold.md
@@ -25,7 +25,7 @@ const metrics = {
totalFiles: props.pr.stats.totalFiles,
additions: props.pr.stats.additions,
deletions: props.pr.stats.deletions,
- directories: props.pr.directories.size,
+ directories: Array.isArray(props.pr.directories) ? props.pr.directories.length : props.pr.directories.size,
};
const actual = metrics[props.metric];
diff --git a/packages/code-review-agent/mod.ts b/packages/code-review-agent/mod.ts
index 47266d56..b3bb9d02 100644
--- a/packages/code-review-agent/mod.ts
+++ b/packages/code-review-agent/mod.ts
@@ -6,8 +6,10 @@
*/
export { parseDiff } from "./src/parse-diff.ts";
-export { parseDiagnostics } from "./src/parse-diagnostics.ts";
-export { parseDoctorResult } from "./src/parse-doctor.ts";
+export { buildDiagnostics, parseDiagnostics } from "./src/parse-diagnostics.ts";
+export { buildDoctorResult, isOxlintCrash, summarizeDoctorProbe } from "./src/doctor.ts";
+export { normalizeDiagnostic, normalizeOxlintOutput } from "./src/parse-oxlint.ts";
+export type { DoctorEnvironment, DoctorProbeInput, DoctorProbeSummary } from "./src/doctor.ts";
export {
buildCleanupAnalysis,
clusterByFile,
diff --git a/packages/code-review-agent/src/doctor.ts b/packages/code-review-agent/src/doctor.ts
new file mode 100644
index 00000000..8312dd47
--- /dev/null
+++ b/packages/code-review-agent/src/doctor.ts
@@ -0,0 +1,98 @@
+import { TYPE_AWARE_RULES } from "./categories.ts";
+import type { DoctorResult, OxlintDiagnostic } from "./types.ts";
+
+const BLOAT_RULES = [
+ "no-unused-vars",
+ "no-inferrable-types",
+ "no-empty-function",
+ "no-empty-object-type",
+ "no-useless-empty-export",
+ "no-unnecessary-type-constraint",
+ "no-unnecessary-parameter-property-assignment",
+ "no-static-only-class",
+ "no-console",
+ "no-debugger",
+ ...TYPE_AWARE_RULES,
+];
+const TYPE_AWARE_RULE_SET = new Set(TYPE_AWARE_RULES);
+
+export interface DoctorProbeSummary {
+ typeAwareAvailable: boolean;
+ filesAnalyzed: number;
+ filesSkipped: number;
+ importErrors: number;
+ bloatRulesAvailable: string[];
+ bloatRulesMissing: string[];
+ recommendation: DoctorResult["recommendation"];
+}
+
+export interface DoctorProbeInput {
+ diagnostics: readonly OxlintDiagnostic[];
+ stderr: string;
+ exitCode: number;
+}
+
+function isImportNoise(diagnostic: OxlintDiagnostic): boolean {
+ return (
+ diagnostic.message.includes("Cannot find module") ||
+ diagnostic.message.includes("cannot find") ||
+ diagnostic.ruleId.includes("import")
+ );
+}
+
+export function isOxlintCrash(stderr: string): boolean {
+ return /panic|oom|out of memory|fatal|segmentation fault/i.test(stderr);
+}
+
+/** Summarize a bounded type-aware probe without retaining its raw output. */
+export function summarizeDoctorProbe(input: DoctorProbeInput): DoctorProbeSummary {
+ const importNoise = input.diagnostics.filter(isImportNoise);
+ const files = new Set(input.diagnostics.map((diagnostic) => diagnostic.file).filter(Boolean));
+ const skippedFiles = new Set(importNoise.map((diagnostic) => diagnostic.file).filter(Boolean));
+ const available = input.exitCode <= 1 && !isOxlintCrash(input.stderr);
+ const ratio = input.diagnostics.length === 0 ? 0 : importNoise.length / input.diagnostics.length;
+ const recommendation = !available
+ ? "syntax-only"
+ : ratio < 0.3
+ ? "type-aware"
+ : "type-aware-filtered";
+
+ return {
+ typeAwareAvailable: available,
+ filesAnalyzed: files.size,
+ filesSkipped: skippedFiles.size,
+ importErrors: importNoise.length,
+ bloatRulesAvailable: available
+ ? [...BLOAT_RULES]
+ : BLOAT_RULES.filter((rule) => !TYPE_AWARE_RULE_SET.has(rule)),
+ bloatRulesMissing: available ? [] : [...TYPE_AWARE_RULES],
+ recommendation,
+ };
+}
+
+export interface DoctorEnvironment {
+ oxlintInstalled: boolean;
+ oxlintVersion: string;
+ tsgolintInstalled: boolean;
+ tsgolintVersion: string;
+ tsconfigExists: boolean;
+ nodeModulesExists: boolean;
+ nativeSpecifiers: DoctorResult["nativeSpecifiers"];
+}
+
+/** Construct the durable Doctor value from bounded environment observations. */
+export function buildDoctorResult(
+ environment: DoctorEnvironment,
+ probe: DoctorProbeSummary,
+): DoctorResult {
+ return {
+ ...environment,
+ ...probe,
+ nativeSpecifiers: {
+ count: environment.nativeSpecifiers.count,
+ files: [...environment.nativeSpecifiers.files],
+ jsr: environment.nativeSpecifiers.jsr,
+ npm: environment.nativeSpecifiers.npm,
+ },
+ };
+}
diff --git a/packages/code-review-agent/src/parse-diagnostics.ts b/packages/code-review-agent/src/parse-diagnostics.ts
index a27630fa..af80b114 100644
--- a/packages/code-review-agent/src/parse-diagnostics.ts
+++ b/packages/code-review-agent/src/parse-diagnostics.ts
@@ -7,124 +7,17 @@
*/
import { categorizeRule } from "./categories.ts";
+import { normalizeOxlintOutput } from "./parse-oxlint.ts";
import type { Diagnostics, DiagnosticGroup, DoctorResult, OxlintDiagnostic, PR } from "./types.ts";
-function emptyDiagnostics(): Diagnostics {
- return {
- groups: [],
- total: 0,
- fileCount: 0,
- ruleCount: 0,
- byCategory: {
- structural: [],
- verbosity: [],
- typeAware: [],
- other: [],
- },
- summary: "",
- density: 0,
- };
-}
-
-function extractDiagnosticsArray(parsed: unknown): unknown[] {
- if (Array.isArray(parsed)) {
- return parsed;
- }
-
- if (parsed && typeof parsed === "object") {
- const direct = (parsed as { diagnostics?: unknown }).diagnostics;
- if (Array.isArray(direct)) {
- return direct;
- }
-
- if (direct && typeof direct === "object") {
- const nested = (direct as { diagnostics?: unknown }).diagnostics;
- if (Array.isArray(nested)) {
- return nested;
- }
- }
- }
-
- return [];
-}
-
-function parseRuleId(code: string): string {
- const match = /\(([^)]+)\)/.exec(code);
- return match?.[1] ?? code;
-}
-
-function normalizeDiagnostic(entry: unknown): OxlintDiagnostic | null {
- if (!entry || typeof entry !== "object") {
- return null;
- }
-
- const diagnostic = entry as Record;
-
- const ruleId =
- typeof diagnostic.ruleId === "string"
- ? diagnostic.ruleId
- : typeof diagnostic.code === "string"
- ? parseRuleId(diagnostic.code)
- : "unknown";
-
- const severity = diagnostic.severity === "error" ? "error" : "warning";
- const message = typeof diagnostic.message === "string" ? diagnostic.message : "";
-
- const file =
- typeof diagnostic.file === "string"
- ? diagnostic.file
- : typeof diagnostic.filename === "string"
- ? diagnostic.filename
- : "";
-
- const firstLabel = Array.isArray(diagnostic.labels) ? diagnostic.labels[0] : undefined;
- const firstSpan =
- firstLabel && typeof firstLabel === "object"
- ? (firstLabel as { span?: unknown }).span
- : undefined;
-
- const line =
- typeof diagnostic.line === "number"
- ? diagnostic.line
- : firstSpan &&
- typeof firstSpan === "object" &&
- typeof (firstSpan as { line?: unknown }).line === "number"
- ? (firstSpan as { line: number }).line
- : 0;
-
- const column =
- typeof diagnostic.column === "number"
- ? diagnostic.column
- : firstSpan &&
- typeof firstSpan === "object" &&
- typeof (firstSpan as { column?: unknown }).column === "number"
- ? (firstSpan as { column: number }).column
- : 0;
-
- return {
- ruleId,
- severity,
- message,
- file,
- line,
- column,
- };
-}
-
/**
- * Parse raw Oxlint JSON output into structured diagnostics.
+ * Group bounded Oxlint diagnostics for review policies.
*/
-export function parseDiagnostics(rawJson: string, pr: PR, doctor: DoctorResult): Diagnostics {
- let raw: OxlintDiagnostic[];
- try {
- const parsed = JSON.parse(rawJson);
- raw = extractDiagnosticsArray(parsed)
- .map((entry) => normalizeDiagnostic(entry))
- .filter((entry): entry is OxlintDiagnostic => entry !== null);
- } catch {
- return emptyDiagnostics();
- }
-
+export function buildDiagnostics(
+ raw: readonly OxlintDiagnostic[],
+ pr: PR,
+ doctor: DoctorResult,
+): Diagnostics {
const filtered =
doctor.recommendation === "type-aware-filtered"
? raw.filter((d) => {
@@ -214,3 +107,10 @@ export function parseDiagnostics(rawJson: string, pr: PR, doctor: DoctorResult):
density,
};
}
+
+/**
+ * Parse raw Oxlint JSON output into structured diagnostics.
+ */
+export function parseDiagnostics(rawJson: string, pr: PR, doctor: DoctorResult): Diagnostics {
+ return buildDiagnostics(normalizeOxlintOutput(rawJson), pr, doctor);
+}
diff --git a/packages/code-review-agent/src/parse-oxlint.ts b/packages/code-review-agent/src/parse-oxlint.ts
new file mode 100644
index 00000000..1aa088df
--- /dev/null
+++ b/packages/code-review-agent/src/parse-oxlint.ts
@@ -0,0 +1,100 @@
+import type { OxlintDiagnostic } from "./types.ts";
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function stringValue(record: Record, key: string): string | undefined {
+ const value = record[key];
+ return typeof value === "string" ? value : undefined;
+}
+
+function numberValue(record: Record, key: string): number | undefined {
+ const value = record[key];
+ return typeof value === "number" ? value : undefined;
+}
+
+function firstSpan(record: Record): Record | undefined {
+ const labels = record.labels;
+ if (!Array.isArray(labels)) {
+ return undefined;
+ }
+ const label = labels[0];
+ if (!isRecord(label) || !isRecord(label.span)) {
+ return undefined;
+ }
+ return label.span;
+}
+
+function ruleIdFromCode(code: string): string {
+ const match = /\(([^)]+)\)/.exec(code);
+ return match?.[1] ?? code;
+}
+
+/** Convert one Oxlint object to the bounded representation used by reviews. */
+export function normalizeDiagnostic(value: unknown): OxlintDiagnostic | undefined {
+ if (!isRecord(value)) {
+ return undefined;
+ }
+
+ const code = stringValue(value, "code");
+ const span = firstSpan(value);
+ return {
+ message: stringValue(value, "message") ?? "",
+ ruleId: stringValue(value, "ruleId") ?? (code ? ruleIdFromCode(code) : "unknown"),
+ severity: stringValue(value, "severity") === "error" ? "error" : "warning",
+ file: stringValue(value, "file") ?? stringValue(value, "filename") ?? "",
+ line: numberValue(value, "line") ?? numberValue(span ?? {}, "line") ?? 0,
+ column: numberValue(value, "column") ?? numberValue(span ?? {}, "column") ?? 0,
+ };
+}
+
+function diagnosticEntries(value: unknown): unknown[] | undefined {
+ if (Array.isArray(value)) {
+ return value;
+ }
+ if (!isRecord(value)) {
+ return undefined;
+ }
+
+ const diagnostics = value.diagnostics;
+ if (Array.isArray(diagnostics)) {
+ return diagnostics;
+ }
+ if (isRecord(diagnostics) && Array.isArray(diagnostics.diagnostics)) {
+ return diagnostics.diagnostics;
+ }
+ return undefined;
+}
+
+/**
+ * Parse and bound Oxlint JSON before it enters an executable document value.
+ * Unknown fields never cross this boundary.
+ */
+export function normalizeOxlintOutput(
+ stdout: string,
+ files?: readonly string[],
+): OxlintDiagnostic[] {
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(stdout);
+ } catch (error) {
+ throw new Error(
+ `Oxlint returned malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+
+ const entries = diagnosticEntries(parsed);
+ if (!entries) {
+ throw new Error("Oxlint JSON did not contain a diagnostics array");
+ }
+
+ const changed = files ? new Set(files) : undefined;
+ return entries.flatMap((entry) => {
+ const diagnostic = normalizeDiagnostic(entry);
+ if (!diagnostic || (changed && !changed.has(diagnostic.file))) {
+ return [];
+ }
+ return [diagnostic];
+ });
+}
diff --git a/packages/code-review-agent/tests/doctor.test.ts b/packages/code-review-agent/tests/doctor.test.ts
new file mode 100644
index 00000000..6621f2bb
--- /dev/null
+++ b/packages/code-review-agent/tests/doctor.test.ts
@@ -0,0 +1,60 @@
+import { describe, it } from "@executablemd/test-support/bdd";
+import { expect } from "@executablemd/test-support/expect";
+import { buildDoctorResult, summarizeDoctorProbe } from "../src/doctor.ts";
+
+describe("Doctor analysis", () => {
+ it("classifies import noise and chooses filtered type-aware mode", function* () {
+ const summary = summarizeDoctorProbe({
+ exitCode: 1,
+ stderr: "",
+ diagnostics: [
+ {
+ message: "Cannot find module x",
+ ruleId: "import/no-unresolved",
+ severity: "error",
+ file: "a.ts",
+ line: 1,
+ column: 1,
+ },
+ {
+ message: "unused",
+ ruleId: "no-unused-vars",
+ severity: "warning",
+ file: "b.ts",
+ line: 2,
+ column: 1,
+ },
+ ],
+ });
+
+ expect(summary.typeAwareAvailable).toBe(true);
+ expect(summary.recommendation).toBe("type-aware-filtered");
+ expect(summary.importErrors).toBe(1);
+ expect(summary.filesAnalyzed).toBe(2);
+ expect(summary.filesSkipped).toBe(1);
+ });
+
+ it("classifies a type-aware crash without exporting raw process output", function* () {
+ const summary = summarizeDoctorProbe({
+ exitCode: 1,
+ stderr: "tsgolint panic: OOM",
+ diagnostics: [],
+ });
+ const result = buildDoctorResult(
+ {
+ oxlintInstalled: true,
+ oxlintVersion: "oxlint 1",
+ tsgolintInstalled: true,
+ tsgolintVersion: "tsgolint 1",
+ tsconfigExists: true,
+ nodeModulesExists: true,
+ nativeSpecifiers: { count: 0, files: [], jsr: 0, npm: 0 },
+ },
+ summary,
+ );
+
+ expect(result.typeAwareAvailable).toBe(false);
+ expect(result.recommendation).toBe("syntax-only");
+ expect(JSON.stringify(result)).not.toContain("panic");
+ });
+});
diff --git a/packages/code-review-agent/tests/parse-diagnostics.test.ts b/packages/code-review-agent/tests/parse-diagnostics.test.ts
index cb02e139..0f00e457 100644
--- a/packages/code-review-agent/tests/parse-diagnostics.test.ts
+++ b/packages/code-review-agent/tests/parse-diagnostics.test.ts
@@ -4,7 +4,7 @@
*/
import { describe, it } from "@executablemd/test-support/bdd";
import { expect } from "@executablemd/test-support/expect";
-import { parseDiagnostics } from "../src/parse-diagnostics.ts";
+import { buildDiagnostics, parseDiagnostics } from "../src/parse-diagnostics.ts";
import type { DoctorResult, OxlintDiagnostic, PR } from "../src/types.ts";
function makePR(additions = 100): PR {
@@ -56,6 +56,13 @@ function makeDiag(ruleId: string, file: string, line = 1, message = ""): OxlintD
}
describe("parseDiagnostics", () => {
+ it("builds directly from normalized diagnostics", function* () {
+ const result = buildDiagnostics([makeDiag("no-console", "src/a.ts")], makePR(), makeDoctor());
+
+ expect(result.total).toBe(1);
+ expect(result.groups[0].ruleId).toBe("no-console");
+ });
+
it("PD1: groups diagnostics by ruleId", function* () {
const raw = JSON.stringify([
makeDiag("no-unused-vars", "src/a.ts"),
@@ -246,12 +253,10 @@ describe("parseDiagnostics", () => {
expect(result.byCategory.structural.map((g) => g.ruleId)).toContain("no-unused-vars");
});
- it("PD8: malformed JSON returns empty diagnostics", function* () {
- const result = parseDiagnostics("not valid json {{{", makePR(), makeDoctor());
-
- expect(result.total).toBe(0);
- expect(result.density).toBe(0);
- expect(result.groups).toHaveLength(0);
+ it("PD8: malformed JSON fails the diagnostic boundary", function* () {
+ expect(() => parseDiagnostics("not valid json {{{", makePR(), makeDoctor())).toThrow(
+ "malformed JSON",
+ );
});
it("sorts groups by count descending", function* () {
diff --git a/packages/code-review-agent/tests/parse-oxlint.test.ts b/packages/code-review-agent/tests/parse-oxlint.test.ts
new file mode 100644
index 00000000..154707bf
--- /dev/null
+++ b/packages/code-review-agent/tests/parse-oxlint.test.ts
@@ -0,0 +1,45 @@
+import { describe, it } from "@executablemd/test-support/bdd";
+import { expect } from "@executablemd/test-support/expect";
+import { normalizeOxlintOutput } from "../src/parse-oxlint.ts";
+
+describe("Oxlint normalization", () => {
+ it("keeps only the bounded diagnostic fields and filters files", function* () {
+ const result = normalizeOxlintOutput(
+ JSON.stringify({
+ diagnostics: [
+ {
+ message: "unused",
+ code: "eslint(no-unused-vars)",
+ severity: "warning",
+ filename: "src/a.ts",
+ labels: [{ span: { line: 4, column: 7 } }],
+ source: "SECRET_SOURCE_EXCERPT",
+ cause: "ARBITRARY_CAUSE",
+ url: "https://example.invalid/diagnostic",
+ },
+ { message: "other", file: "src/b.ts" },
+ ],
+ stderr: "UNBOUNDED_STDERR",
+ }),
+ ["src/a.ts"],
+ );
+
+ expect(result).toEqual([
+ {
+ message: "unused",
+ ruleId: "no-unused-vars",
+ severity: "warning",
+ file: "src/a.ts",
+ line: 4,
+ column: 7,
+ },
+ ]);
+ });
+
+ it("rejects malformed or unsupported output", function* () {
+ expect(() => normalizeOxlintOutput("not json")).toThrow("malformed JSON");
+ expect(() => normalizeOxlintOutput(JSON.stringify({ output: [] }))).toThrow(
+ "diagnostics array",
+ );
+ });
+});
diff --git a/scripts/tests/review-infrastructure.test.ts b/scripts/tests/review-infrastructure.test.ts
new file mode 100644
index 00000000..c814391e
--- /dev/null
+++ b/scripts/tests/review-infrastructure.test.ts
@@ -0,0 +1,307 @@
+import { describe, it } from "@executablemd/test-support/bdd";
+import { expect } from "@executablemd/test-support/expect";
+import { FetchApi, type FetchResponse } from "@effectionx/fetch";
+import { readTextFile } from "@effectionx/fs";
+import { InMemoryStream } from "@executablemd/durable-streams";
+import { API } from "@executablemd/runtime";
+import { useStubFs } from "@executablemd/runtime/test";
+import { execute } from "../../packages/core/src/execute.ts";
+import { useTempFileCompiler } from "../../packages/core/src/temp-file-compiler.ts";
+import { forEach } from "@effectionx/stream-helpers";
+import { scoped, until } from "effection";
+import type { Operation } from "effection";
+
+interface RequestRecord {
+ input: string;
+ init?: RequestInit;
+ shouldExpect: boolean;
+}
+
+function response(body: string): FetchResponse {
+ const raw = new Response(body, { status: 200 });
+ return {
+ raw,
+ get bodyUsed() {
+ return raw.bodyUsed;
+ },
+ get ok() {
+ return raw.ok;
+ },
+ get status() {
+ return raw.status;
+ },
+ get statusText() {
+ return raw.statusText;
+ },
+ get headers() {
+ return raw.headers;
+ },
+ get url() {
+ return raw.url;
+ },
+ get redirected() {
+ return raw.redirected;
+ },
+ get type() {
+ return raw.type;
+ },
+ *json(parse?: (value: unknown) => T): Operation {
+ const value: unknown = JSON.parse(body);
+ return parse ? parse(value) : (value as T);
+ },
+ *text(): Operation {
+ return body;
+ },
+ *arrayBuffer(): Operation {
+ return yield* until(raw.arrayBuffer());
+ },
+ *blob(): Operation {
+ return yield* until(raw.blob());
+ },
+ *formData(): Operation {
+ return yield* until(raw.formData());
+ },
+ body() {
+ throw new Error("body streaming is not used by this test");
+ },
+ *expect(): Operation {
+ return this;
+ },
+ };
+}
+
+function* run(
+ provider: string,
+ token?: string,
+): Operation<{ requests: RequestRecord[]; journal: string }> {
+ const requests: RequestRecord[] = [];
+ const files: Record = {
+ "doc.md": [
+ "",
+ "",
+ '',
+ '',
+ '',
+ "",
+ "",
+ '',
+ ].join("\n"),
+ "components/GitHubAuth.md": provider,
+ "components/Probe.md": [
+ "---",
+ "props:",
+ " type: object",
+ " properties:",
+ " label: { type: string }",
+ " url: { type: string }",
+ " required: [label, url]",
+ " additionalProperties: false",
+ "---",
+ "",
+ "```ts eval",
+ "const value = yield* fetch(url, { method: 'POST', headers: { 'X-Caller': 'kept', 'Content-Type': 'application/custom' }, body: 'request-body' }).expect().json();",
+ "```",
+ "",
+ "{label}={value.message}",
+ ].join("\n"),
+ "components/Wrapper.md": '',
+ "components/NoExpect.md": [
+ "```ts eval",
+ 'yield* fetch("https://api.github.com/repos/no-expect");',
+ "```",
+ ].join("\n"),
+ };
+
+ yield* useStubFs(files);
+ yield* API.Env.around({
+ *env([name], next) {
+ if (name === "GITHUB_TOKEN") {
+ return token;
+ }
+ return yield* next(name);
+ },
+ });
+ yield* FetchApi.around(
+ {
+ *fetch([input, init, shouldExpect]) {
+ const request =
+ input instanceof Request ? input.url : input instanceof URL ? input.href : input;
+ requests.push({ input: request, init, shouldExpect });
+ return response(JSON.stringify({ message: "ok" }));
+ },
+ },
+ { at: "min" },
+ );
+
+ const stream = new InMemoryStream();
+ const execution = yield* execute({ path: "doc.md", stream, componentDirs: ["components"] });
+ yield* forEach(function* () {}, execution.output);
+ const result = yield* execution;
+ expect(result.ok).toBe(true);
+ return { requests, journal: JSON.stringify(stream.snapshot()) };
+}
+
+function* runDocument(
+ document: string,
+ components: Record,
+ processResult?: { exitCode: number; stdout: string; stderr: string },
+ componentDirs: string[] = ["components"],
+): Operation {
+ return yield* scoped(function* () {
+ yield* useStubFs({ "doc.md": document, ...components });
+ if (processResult) {
+ yield* API.Process.around({
+ *exec() {
+ return processResult;
+ },
+ });
+ }
+ const stream = new InMemoryStream();
+ const execution = yield* execute({ path: "doc.md", stream, componentDirs });
+ yield* forEach(function* () {}, execution.output);
+ const result = yield* execution;
+ return result.ok;
+ });
+}
+
+describe("review infrastructure", () => {
+ it("uses the real GitHubAuth component for exact-host scoped middleware", function* () {
+ yield* useTempFileCompiler();
+ const provider = yield* readTextFile(".reviews/components/GitHubAuth.md");
+ const result = yield* run(provider, "secret-token");
+
+ expect(result.requests).toHaveLength(6);
+ expect(result.requests[0].input).toBe("https://api.github.com/repos/nested");
+ expect(result.requests[0].init?.headers).toBeInstanceOf(Headers);
+ const nestedHeaders = new Headers(result.requests[0].init?.headers);
+ expect(nestedHeaders.get("Authorization")).toBe("Bearer secret-token");
+ expect(nestedHeaders.get("Accept")).toBe("application/vnd.github+json");
+ expect(nestedHeaders.get("X-Caller")).toBe("kept");
+ expect(result.requests[0].init?.body).toBe("request-body");
+ expect(result.requests[0].shouldExpect).toBe(true);
+ expect(new Headers(result.requests[1].init?.headers).get("Authorization")).toBe(null);
+ expect(new Headers(result.requests[2].init?.headers).get("Authorization")).toBe(null);
+ expect(new Headers(result.requests[3].init?.headers).get("Authorization")).toBe(null);
+ expect(new Headers(result.requests[4].init?.headers).get("Authorization")).toBe(
+ "Bearer secret-token",
+ );
+ expect(result.requests[4].shouldExpect).toBe(false);
+ expect(new Headers(result.requests[5].init?.headers).get("Authorization")).toBe(null);
+ expect(result.journal).not.toContain("secret-token");
+ });
+
+ it("delegates unchanged when the token is unavailable and restores scope", function* () {
+ yield* useTempFileCompiler();
+ const provider = yield* readTextFile(".reviews/components/GitHubAuth.md");
+ const result = yield* run(provider);
+
+ for (const request of result.requests) {
+ expect(new Headers(request.init?.headers).get("Authorization")).toBe(null);
+ if (request.init?.body !== undefined) {
+ expect(request.init.body).toBe("request-body");
+ }
+ }
+ expect(result.journal).not.toContain("secret-token");
+ });
+
+ it("fails on malformed and unexpected Oxlint results, but skips empty input", function* () {
+ yield* useTempFileCompiler();
+
+ expect(
+ yield* runDocument(
+ '',
+ { ".reviews/components/OxlintDiagnostics.ts": "" },
+ { exitCode: 0, stdout: "not json", stderr: "" },
+ [".reviews/components"],
+ ),
+ ).toBe(false);
+ expect(
+ yield* runDocument(
+ '',
+ { ".reviews/components/OxlintDiagnostics.ts": "" },
+ { exitCode: 2, stdout: "[]", stderr: "invocation failed" },
+ [".reviews/components"],
+ ),
+ ).toBe(false);
+
+ let calls = 0;
+ yield* API.Process.around({
+ *exec() {
+ calls++;
+ return { exitCode: 2, stdout: "", stderr: "must not run" };
+ },
+ });
+ expect(
+ yield* runDocument(
+ '',
+ {
+ ".reviews/components/OxlintDiagnostics.ts": "",
+ },
+ undefined,
+ [".reviews/components"],
+ ),
+ ).toBe(true);
+ expect(calls).toBe(0);
+ });
+
+ it("fails when a provider returns 2xx without model content", function* () {
+ yield* useTempFileCompiler();
+ const deepInfra = yield* readTextFile(".reviews/components/DeepInfraProvider.md");
+ const ollama = yield* readTextFile(".reviews/components/OllamaProvider.md");
+ const sample = yield* readTextFile("packages/core/components/Sample.md");
+ yield* FetchApi.around({
+ *fetch() {
+ return response(JSON.stringify({ choices: [] }));
+ },
+ });
+
+ expect(
+ yield* runDocument(
+ '',
+ {
+ "components/DeepInfraProvider.md": deepInfra,
+ "components/Sample.md": sample,
+ },
+ ),
+ ).toBe(false);
+ expect(
+ yield* runDocument(
+ '',
+ {
+ "components/OllamaProvider.md": ollama,
+ "components/Sample.md": sample,
+ },
+ ),
+ ).toBe(false);
+ });
+
+ it("requires GitHubComment metadata", function* () {
+ yield* useTempFileCompiler();
+ const comment = yield* readTextFile(".reviews/components/GitHubComment.md");
+ expect(
+ yield* runDocument("", {
+ "components/GitHubComment.md": comment,
+ }),
+ ).toBe(false);
+ });
+
+ it("executes the real RepositoryInventory function component", function* () {
+ yield* useTempFileCompiler();
+ yield* API.Fs.around({
+ *glob() {
+ return [{ path: "packages/example.ts", isFile: true, isDirectory: false }];
+ },
+ });
+ expect(
+ yield* runDocument(
+ '\n{inventory.fileCount}:{inventory.lineCount}',
+ {
+ ".reviews/components/RepositoryInventory.ts": "",
+ "packages/example.ts": "first\nsecond\n",
+ },
+ undefined,
+ [".reviews/components"],
+ ),
+ ).toBe(true);
+ });
+});
diff --git a/specs/code-review-agent-spec.md b/specs/code-review-agent-spec.md
index aac4620c..96a78e72 100644
--- a/specs/code-review-agent-spec.md
+++ b/specs/code-review-agent-spec.md
@@ -254,6 +254,16 @@ scope.around(Sample, function* ([context], next) {
### 4.4 `GitHubComment.md`
+CI roots place this component inside `` and `