diff --git a/.github/workflows/repo-analysis.yml b/.github/workflows/repo-analysis.yml index 59454169c..a20472809 100644 --- a/.github/workflows/repo-analysis.yml +++ b/.github/workflows/repo-analysis.yml @@ -22,6 +22,7 @@ jobs: timeout-minutes: 25 permissions: contents: read + issues: write steps: - uses: actions/checkout@v6 with: @@ -36,6 +37,8 @@ jobs: - name: Run repo analysis env: DEEPINFRA_TOKEN: ${{ secrets.DEEPINFRA_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} run: deno run --allow-all cli/src/cli.ts run .reviews/AnalyzeRepoCI.md --component-dir .reviews/components --component-dir .reviews/policies --component-dir core/components -j .reviews/journal.analyze.ci.jsonl > .reviews/analyze-report.md - name: Write run metadata diff --git a/.reviews/components/CleanupIssues.md b/.reviews/components/CleanupIssues.md new file mode 100644 index 000000000..f99f11936 --- /dev/null +++ b/.reviews/components/CleanupIssues.md @@ -0,0 +1,152 @@ +--- +inputs: + cleanupAnalysis: + type: object + required: true + diagnostics: + type: object + required: true +--- + +```ts persist eval +const token = process.env.GITHUB_TOKEN; +const repo = process.env.GITHUB_REPOSITORY; + +if (!token || !repo) { + return ""; +} + +const [owner, name] = repo.split("/"); +const api = `https://api.github.com/repos/${owner}/${name}`; +const headers = { + "Authorization": `Bearer ${token}`, + "Accept": "application/vnd.github+json", + "Content-Type": "application/json", +}; + +const LABEL = "ema-cleanup"; +const TOP_N = 5; + +// 1. Ensure label exists +const labelsResult = yield* fetch(`${api}/labels/${LABEL}`, { headers }) + .json() + .catch(() => null); + +if (!labelsResult || labelsResult.message) { + yield* fetch(`${api}/labels`, { + method: "POST", + headers, + body: JSON.stringify({ + name: LABEL, + description: "Auto-generated cleanup finding from repo analysis", + color: "d4c5f9", + }), + }).expect(); +} + +// 2. Fetch all open issues with ema-cleanup label +const existingIssues = []; +let page = 1; +while (true) { + const batch = yield* fetch( + `${api}/issues?labels=${LABEL}&state=open&per_page=100&page=${page}`, + { headers }, + ).expect().json(); + + if (!Array.isArray(batch) || batch.length === 0) break; + existingIssues.push(...batch); + if (batch.length < 100) break; + page++; +} + +// 3. Build marker → issue map +const markerRe = //; +const issuesByFile = new Map(); +for (const issue of existingIssues) { + const match = issue.body?.match(markerRe); + if (match) { + issuesByFile.set(match[1], issue); + } +} + +// 4. Process top 5 clusters +const topClusters = cleanupAnalysis.fileClusters.slice(0, TOP_N); +const topFiles = new Set(topClusters.map(c => c.file)); + +let created = 0; +let updated = 0; +let closed = 0; + +for (const cluster of topClusters) { + const marker = ``; + const title = `cleanup(${cluster.kind}): ${cluster.file} — ${cluster.totalViolations} violations, ${cluster.coOccurrence} rules`; + + const bodyLines = [ + marker, + "", + `## Cleanup: \`${cluster.file}\``, + "", + "| Metric | Value |", + "|---|---|", + `| Score | ${cluster.score} |`, + `| Kind | ${cluster.kind} |`, + `| Violations | ${cluster.totalViolations} |`, + `| Co-occurring rules | ${cluster.coOccurrence} |`, + `| Structural | ${cluster.categories.structural} |`, + `| Verbosity | ${cluster.categories.verbosity} |`, + `| Type-aware | ${cluster.categories.typeAware} |`, + `| Other | ${cluster.categories.other} |`, + "", + `**Top rules:** ${cluster.ruleIds.slice(0, 15).join(", ")}${cluster.ruleIds.length > 15 ? ` (+${cluster.ruleIds.length - 15} more)` : ""}`, + "", + "---", + `Generated by EMA repo analysis · Last updated: ${new Date().toISOString()}`, + ]; + const body = bodyLines.join("\n"); + + const existing = issuesByFile.get(cluster.file); + + if (existing) { + yield* fetch(`${api}/issues/${existing.number}`, { + method: "PATCH", + headers, + body: JSON.stringify({ title, body }), + }).expect(); + updated++; + } else { + yield* fetch(`${api}/issues`, { + method: "POST", + headers, + body: JSON.stringify({ + title, + body, + labels: [LABEL], + }), + }).expect(); + created++; + } +} + +// 5. Close issues whose files are no longer in top 5 +for (const [file, issue] of issuesByFile.entries()) { + if (!topFiles.has(file)) { + yield* fetch(`${api}/issues/${issue.number}/comments`, { + method: "POST", + headers, + body: JSON.stringify({ + body: "Resolved — file no longer in top-5 cleanup targets. Closing automatically.", + }), + }).expect(); + + yield* fetch(`${api}/issues/${issue.number}`, { + method: "PATCH", + headers, + body: JSON.stringify({ state: "closed" }), + }).expect(); + closed++; + } +} + +const summary = `Cleanup issues: created ${created}, updated ${updated}, closed ${closed}`; +return summary; +``` diff --git a/.reviews/components/RepoPolicyReport.md b/.reviews/components/RepoPolicyReport.md index 5211e37bc..72c63b068 100644 --- a/.reviews/components/RepoPolicyReport.md +++ b/.reviews/components/RepoPolicyReport.md @@ -27,3 +27,5 @@ inputs: + + diff --git a/specs/code-review-agent-spec.md b/specs/code-review-agent-spec.md index 7d23fb377..8257836ba 100644 --- a/specs/code-review-agent-spec.md +++ b/specs/code-review-agent-spec.md @@ -1236,11 +1236,36 @@ compatibility since tsgo uses Node module resolution. `.github/pull_request_template.md` — scope confirmation, Rule of Three checklist for new abstractions, dependency justification. -### 13.8 Additional files +### 13.8 Cleanup issues (`CleanupIssues.md`) + +The repo analysis pipeline creates idempotent GitHub issues for +the top 5 file clusters ranked by `buildCleanupAnalysis()`. + +**Identity:** Each issue body contains a marker comment +``. The component searches open issues +with the `ema-cleanup` label for matching markers before creating. + +**Lifecycle:** + +| Existing issue? | File in top 5? | Action | +|---|---|---| +| No | Yes | Create new issue | +| Yes | Yes | Update title + body with latest stats | +| Yes | No | Close with resolution comment | + +Issues are driven entirely by deterministic cluster data — file +path, score, violation count, co-occurring rule count, category +breakdown. No LLM output parsing. + +Local runs skip issue creation (no `GITHUB_TOKEN` → return empty). + +### 13.9 Additional files ``` .reviews/ .oxlintrc.json Sensor config (committed) + components/ + CleanupIssues.md Idempotent GitHub issue lifecycle .github/ pull_request_template.md Process enforcement