Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/repo-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
timeout-minutes: 25
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
with:
Expand All @@ -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
Expand Down
152 changes: 152 additions & 0 deletions .reviews/components/CleanupIssues.md
Original file line number Diff line number Diff line change
@@ -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 = /<!-- ema-cleanup:(.+?) -->/;
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 = `<!-- ema-cleanup:${cluster.file} -->`;
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)` : ""}`,
"",
"---",
`<sub>Generated by EMA repo analysis · Last updated: ${new Date().toISOString()}</sub>`,
];
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;
```
2 changes: 2 additions & 0 deletions .reviews/components/RepoPolicyReport.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ inputs:
<OxlintSummary diagnostics={diagnostics} doctor={doctor} />

<RepoCleanupPolicy diagnostics={diagnostics} doctor={doctor} fileList={fileList} cleanupAnalysis={cleanupAnalysis} />

<CleanupIssues cleanupAnalysis={cleanupAnalysis} diagnostics={diagnostics} />
27 changes: 26 additions & 1 deletion specs/code-review-agent-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<!-- ema-cleanup:{file} -->`. 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
Expand Down
Loading