From b84445a8023e04f2a71195d44f43eb579b828efb Mon Sep 17 00:00:00 2001 From: kestarsheng Date: Thu, 10 Sep 2026 20:42:39 +0800 Subject: [PATCH 01/15] submit: kestarsheng-code-review-agent - AI-powered code review service (FastAPI + DeepSeek + FastMCP) - POST /v1/review returns structured quality report (score, grade, issues) - Live API: https://code-review-agent-ashy-six.vercel.app - Health check and verification endpoints with commit binding - Full source code included in source/ --- .../kestarsheng-code-review-agent/RIGHTS.md | 24 +++ .../SUBMISSION.md | 59 +++++++ .../source/.env.example | 10 ++ .../source/.gitignore | 23 +++ .../source/Dockerfile | 16 ++ .../source/README.md | 78 +++++++++ .../source/api/index.py | 1 + .../source/app/__init__.py | 2 + .../source/app/config.py | 33 ++++ .../source/app/main.py | 90 ++++++++++ .../source/app/mcp_server.py | 55 ++++++ .../source/app/prompts.py | 44 +++++ .../source/app/reviewer.py | 71 ++++++++ .../source/app/schemas.py | 58 +++++++ .../source/render.yaml | 18 ++ .../source/requirements.txt | 8 + .../source/tests/test_app.py | 79 +++++++++ .../source/vercel.json | 17 ++ .../source/web/index.html | 156 ++++++++++++++++++ .../submission.json | 10 ++ .../verification/README.md | 83 ++++++++++ 21 files changed, 935 insertions(+) create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/render.yaml create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/requirements.txt create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/vercel.json create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md new file mode 100644 index 0000000..d3183ab --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/RIGHTS.md @@ -0,0 +1,24 @@ +# Submission rights declaration + +Project: `Code Review Agent` +Submission slug: `kestarsheng-code-review-agent` +Submitter: `刘宇珂 (kestarsheng)` +Date: `2026-09-10` + +The submitter confirms that they own, or have sufficient authorization for, the source code, dependencies, service, data, branding, and other materials submitted in this pull request. + +Subject to the official program terms, the submitter authorizes X-Agent to retain, reproduce, audit, test, archive, and publish the submitted program artifact for judging, fraud prevention, dispute handling, ecosystem submission, and post-award accountability. Closing the pull request, deleting a fork, or deleting an external repository does not revoke the official archive rights attached to an accepted and rewarded entry. + +Third-party components and their licenses: +- `fastapi` — MIT License +- `uvicorn` — BSD-3-Clause License +- `openai` (Python SDK) — MIT License +- `httpx` — BSD-3-Clause License +- `pydantic` / `pydantic-settings` — MIT License +- `python-dotenv` — BSD-3-Clause License +- `fastmcp` — MIT License +- `pytest` — MIT License + +Exceptions or restrictions: None. All source code in `source/` is original work of the submitter. The service calls DeepSeek's hosted LLM API at runtime; no DeepSeek software is included in the submission. + +This template is an operational declaration, not a substitute for event terms reviewed by qualified counsel. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md new file mode 100644 index 0000000..cc35647 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -0,0 +1,59 @@ +# Code Review Agent + +## Capability + +- **One-line description:** Review source code and return a structured quality report (correctness, security, performance, maintainability, best practices) so AI-generated code can be checked before merge. +- **Who it helps:** Developers using AI coding tools (Claude Code, Codex, Cursor) and any AI Agent that needs a code-quality gate. +- **Capability boundary:** Accepts a single code snippet (up to 60 000 chars) plus optional language and context. Returns a JSON report with score (0-100), grade (A-D), issues, strengths and improvements. Does not execute, compile, or persist submitted code. Does not review entire repositories or run static-analysis binaries. + +## Live API + +- **API base URL:** https://code-review-agent-ashy-six.vercel.app/v1 +- **Health-check URL:** https://code-review-agent-ashy-six.vercel.app/health +- **Authentication:** none +- **Rate limits / known limits:** Single request limited by LLM provider timeout (120 s). Max code size 60 000 chars. Free-tier hosting may cold-start. +- **API contract:** OpenAPI at `/docs`; request `POST /v1/review` body `{"code": string, "language"?: string, "context"?: string}`, response `{"ok": true, "language": string, "model": string, "report": ReviewReport}`. + +## Source and reproducibility + +- **Source repository:** https://github.com/kestarsheng/code-review-agent +- **Review commit:** `64b3365f0faad5b71f6710cd5dd0356c94262d74` +- **Source submitted in this PR:** `source/` +- **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` +- **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` +- **Deploy:** `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent`, Render Blueprint from `render.yaml`, or Vercel (current production deployment). +- **Version binding:** `GET /health` returns `{"status":"ok","commit":""}`; `GET /.well-known/xagent-verification.json` returns `{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":""}`. The commit is injected via the `COMMIT` environment variable at deploy time. + +The API must expose: + +```json +// GET /health +{"status":"ok","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +``` + +```json +// GET /.well-known/xagent-verification.json +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +``` + +## Verification + +The reproducible call instructions and redacted example responses are in `verification/README.md`. + +- **Health-check result:** `{"status":"ok","commit":"64b3365..."}` +- **Capability call:** `POST /v1/review` with `{"code":"def f(x): return x/0","language":"python"}` +- **Expected error behavior:** Empty body → 422; oversized code → 413; LLM failure → 502 `{"ok":false,"error":"..."}`. + +## Security and data handling + +- **Data collected:** Submitted code snippet, language hint, optional context. No authentication, no user identifiers. +- **Purpose and retention:** Code is sent to the configured LLM provider (DeepSeek) for review only. The service does not persist submitted code to any database or log. +- **Third parties / outbound network calls:** DeepSeek API (OpenAI-compatible protocol) for LLM inference. +- **Secrets:** No secrets are committed. `LLM_API_KEY` is set as a deployment environment variable and never appears in source. +- **Known risks / restrictions:** Vercel serverless functions have a 10 s default timeout; long code reviews may approach this limit. The LLM may occasionally produce imperfect JSON; the parser tolerates fenced/embedded JSON. + +## Support + +- **Team / builder:** kestarsheng (刘宇珂) +- **Contact:** 2410251355@henu.edu.cn +- **License / rights:** UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. Submitter owns all source and authorizes review and post-award retention. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example new file mode 100644 index 0000000..f9b74b3 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.env.example @@ -0,0 +1,10 @@ +# LLM (OpenAI-compatible protocol) +LLM_BASE_URL=https://api.deepseek.com/v1 +LLM_API_KEY=sk-xxxxxxxxxxxxxxxx +LLM_MODEL=deepseek-chat +LLM_TIMEOUT_SECONDS=120 + +# Deployment identification +COMMIT=dev +HOST=0.0.0.0 +PORT=8000 \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore new file mode 100644 index 0000000..e2d9c48 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ + +# Environment & secrets +.env +.env.* +!.env.example + +# IDE / OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# Build / logs +dist/ +build/ +*.log +.vercel diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile new file mode 100644 index 0000000..53edb26 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app +COPY web ./web + +ENV HOST=0.0.0.0 +ENV PORT=7860 + +EXPOSE 7860 + +CMD ["python", "-m", "app.main"] \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md new file mode 100644 index 0000000..c877903 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md @@ -0,0 +1,78 @@ +# Code Review Agent + +AI 代码质量评审服务(Code Review as a Service)。把代码送给 LLM,返回结构化质量报告,亦可作为 MCP 工具被 Claude Code / Codex / Cursor 等 Agent 调用。 + +> Submission for **X-Agent AI MCP Hackathon 2026 · Open Innovation Challenge**. + +## What it does + +- `POST /v1/review` — 提交代码片段,返回结构化评审报告(正确性 / 安全 / 性能 / 可维护性 / 最佳实践) +- `GET /health` — 健康检查,返回当前部署 Commit +- `GET /.well-known/xagent-verification.json` — 部署证明(slug + commit) +- `GET /` — 在线演示页(粘贴代码即时出报告) +- MCP 工具 `review_code` — 供 Agent 调用(stdio / streamable HTTP) + +## Quick start (local) + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env # 填入 LLM_API_KEY 等 +uvicorn app.main:app --reload +``` + +Open http://127.0.0.1:8000 (demo page) or http://127.0.0.1:8000/docs (Swagger). + +Example call: + +```bash +curl -X POST http://127.0.0.1:8000/v1/review \ + -H "Content-Type: application/json" \ + -d '{"code": "def foo(a, b):\n return a / b", "language": "python"}' +``` + +## MCP usage + +```bash +python -m app.mcp_server # stdio transport for Claude Code / Codex / Cursor +``` + +Or register in your client config: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "python", + "args": ["-m", "app.mcp_server"] + } + } +} +``` + +## Configuration (env vars) + +| Var | Default | Description | +| --- | --- | --- | +| `LLM_BASE_URL` | `https://api.deepseek.com/v1` | OpenAI-compatible base URL | +| `LLM_API_KEY` | — | API key (required) | +| `LLM_MODEL` | `deepseek-chat` | Model name | +| `LLM_TIMEOUT_SECONDS` | `120` | LLM request timeout | +| `COMMIT` | `dev` | Deployed commit, returned by /health and /.well-known/xagent-verification.json | + +## Deployment + +- Docker: `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent` +- Render: push to repo, set env vars, use `render.yaml` — set `COMMIT` to the exact deployed commit. + +After deploy, verify: + +```bash +curl https:///health +curl https:///.well-known/xagent-verification.json +``` + +## License + +UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py new file mode 100644 index 0000000..732c1c3 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/api/index.py @@ -0,0 +1 @@ +from app.main import app \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py new file mode 100644 index 0000000..690abf7 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Code Review Agent application package.""" \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py new file mode 100644 index 0000000..94aaf1b --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/config.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Application configuration loaded from environment variables.""" +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + +PROJECT_SLUG = "kestarsheng-code-review-agent" + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", env_file_encoding="utf-8", extra="ignore" + ) + + # LLM (OpenAI-compatible protocol: DeepSeek / Qwen / etc.) + llm_base_url: str = "https://api.deepseek.com/v1" + llm_api_key: str = "" + llm_model: str = "deepseek-chat" + llm_timeout_seconds: float = 120.0 + + # Deployment identification + commit: str = "dev" + host: str = "0.0.0.0" + port: int = 8000 + + # Safety + max_code_chars: int = 60_000 + max_response_chars: int = 20_000 + + +@lru_cache +def get_settings() -> Settings: + return Settings() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py new file mode 100644 index 0000000..8428539 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""FastAPI application entry point. + +Provides: +- POST /v1/review review source code and return a structured report +- GET /health health check returning the deployed commit +- GET /.well-known/xagent-verification.json deployment proof +- GET / minimal web demo page +""" +import logging +from pathlib import Path + +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import HTMLResponse, JSONResponse + +from .config import PROJECT_SLUG, get_settings +from .reviewer import ReviewError, review_code +from .schemas import ( + HealthResponse, + ReviewRequest, + ReviewResponse, + VerificationResponse, +) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +settings = get_settings() +app = FastAPI( + title="Code Review Agent", + description="AI code quality review as a service. " + "Send code, get a structured review report.", + version="1.0.0", +) + + +@app.get("/health", response_model=HealthResponse, tags=["meta"]) +def health() -> HealthResponse: + return HealthResponse(status="ok", commit=settings.commit) + + +@app.get( + "/.well-known/xagent-verification.json", + response_model=VerificationResponse, + tags=["meta"], +) +def verification() -> VerificationResponse: + return VerificationResponse( + schemaVersion=1, slug=PROJECT_SLUG, commit=settings.commit + ) + + +@app.post("/v1/review", response_model=ReviewResponse, tags=["review"]) +async def review(req: ReviewRequest) -> ReviewResponse: + if len(req.code) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"code 过长(限制 {settings.max_code_chars} 字符)", + ) + try: + report = review_code(code=req.code, language=req.language, context=req.context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + return ReviewResponse(language=req.language, model=settings.llm_model, report=report) + + +@app.get("/", response_class=HTMLResponse, include_in_schema=False) +def index() -> HTMLResponse: + html = Path(__file__).resolve().parent.parent / "web" / "index.html" + if html.exists(): + return HTMLResponse(html.read_text(encoding="utf-8")) + return HTMLResponse("

Code Review Agent

See /docs for API.

") + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"ok": False, "error": exc.detail}, + ) + + +def main() -> None: + import uvicorn + + uvicorn.run("app.main:app", host=settings.host, port=settings.port, reload=False) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py new file mode 100644 index 0000000..bf05966 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +"""FastMCP server exposing the code review capability as a reusable tool. + +Run via stdio (default) for Claude Code / Codex / Cursor, or via +streamable HTTP transport on the same service. +""" +import json + +from fastmcp import FastMCP + +from .config import PROJECT_SLUG +from .reviewer import ReviewError, review_code + +mcp = FastMCP( + PROJECT_SLUG, + instructions=( + "Code quality review assistant. Review source code and return a " + "structured report covering correctness, security, performance, " + "maintainability and best practices." + ), +) + + +@mcp.tool() +def review_code( + code: str, + language: str = "", + context: str = "", +) -> str: + """Review the given source code and return a structured quality report. + + Args: + code: source code to review. + language: programming language hint (python, java, js, go, ...). + context: optional description of what the code is supposed to do. + + Returns: + A JSON string with summary, score (0-100), grade (A-D), issues, + strengths and improvements. + """ + try: + report = review_code(code=code, language=language, context=context) + except ReviewError as exc: + return json.dumps( + {"ok": False, "error": str(exc)}, ensure_ascii=False + ) + return json.dumps({"ok": True, "report": report}, ensure_ascii=False) + + +def main() -> None: + mcp.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py new file mode 100644 index 0000000..316d708 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""Prompt templates for the code review engine.""" + +SYSTEM_PROMPT = """\ +你是一名资深软件架构师与代码评审专家,擅长从正确性、安全性、性能、\ +可维护性和最佳实践五个维度评审代码。你输出的评审结论必须基于代码事实,\ +不得臆造。请以严格的 JSON 格式输出评审报告,不要输出任何 JSON 以外的内容。 +""" + +JSON_SCHEMA_EXAMPLE = """\ +评审报告 JSON 结构如下: +{ + "summary": "一段 2-4 句的总体评价,指出最关键的问题与整体质量", + "score": 0到100的整数, + "grade": "由 score 派生,90-100 为 A,75-89 为 B,60-74 为 C,60 以下为 D", + "issues": [ + { + "severity": "critical 或 major 或 minor 或 info", + "category": "correctness 或 security 或 performance 或 maintainability 或 best_practice", + "line": 问题所在的大致行号(1-based),无法确定时填 null, + "title": "一句话问题标题", + "description": "问题详细描述,说明为什么是问题、可能后果", + "suggestion": "具体可行的修复建议,尽量给出示例代码" + } + ], + "strengths": ["代码优点列表,至少 1 条"], + "improvements": ["改进方向列表,至少 1 条"] +} + +规则: +1. issues 中必须至少包含一条真正存在的问题;若确实没有问题,severity 用 info 说明代码状态优秀。 +2. 不要编造代码中不存在的问题。security 类别优先于其他类别报告。 +3. 建议要具体、可执行,需要时可附简短示例代码。 +""" + + +def build_user_prompt(language: str, context: str, code: str) -> str: + parts = [f"语言:{language or '未知'}"] + if context: + parts.append(f"任务上下文:{context}") + parts.append("待评审代码:") + parts.append("```" + language + "\n" + code + "\n```") + parts.append(JSON_SCHEMA_EXAMPLE) + return "\n".join(parts) \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py new file mode 100644 index 0000000..b169749 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""Core code review engine: calls an OpenAI-compatible LLM and returns a +structured JSON report.""" +import json +import logging +import re +from typing import Any + +from openai import OpenAI + +from .config import get_settings +from .prompts import SYSTEM_PROMPT, build_user_prompt + +logger = logging.getLogger(__name__) + + +class ReviewError(Exception): + pass + + +def _extract_json(text: str) -> dict[str, Any]: + """Extract the first JSON object from a model response (tolerates fences).""" + text = text.strip() + if text.startswith("```"): + text = re.sub(r"^```[a-zA-Z]*\s*", "", text) + text = re.sub(r"\s*```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + match = re.search(r"\{.*\}", text, re.DOTALL) + if not match: + raise ReviewError("模型未返回有效 JSON") + return json.loads(match.group(0)) + + +def review_code(code: str, language: str = "", context: str = "") -> dict[str, Any]: + """Run the code review against the configured LLM. + + Returns the raw dict from the model after basic validation. + """ + settings = get_settings() + if not settings.llm_api_key: + raise ReviewError("LLM API Key 未配置(环境变量 LLM_API_KEY)") + + client = OpenAI( + base_url=settings.llm_base_url, + api_key=settings.llm_api_key, + timeout=settings.llm_timeout_seconds, + ) + user_prompt = build_user_prompt(language, context, code) + + try: + resp = client.chat.completions.create( + model=settings.llm_model, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_prompt}, + ], + temperature=0.2, + max_tokens=4000, + ) + except Exception as exc: # noqa: BLE001 + logger.exception("LLM call failed") + raise ReviewError(f"LLM 调用失败: {exc}") from exc + + content = resp.choices[0].message.content or "{}" + data = _extract_json(content) + + if not isinstance(data, dict) or "issues" not in data: + raise ReviewError("模型返回结构不完整,缺少 issues 字段") + return data \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py new file mode 100644 index 0000000..b2dfcf8 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""Pydantic schemas for the code review service.""" +from typing import Literal + +from pydantic import BaseModel, Field + +Severity = Literal["critical", "major", "minor", "info"] +Category = Literal[ + "correctness", "security", "performance", "maintainability", "best_practice" +] + + +class ReviewRequest(BaseModel): + code: str = Field(..., min_length=1, description="Source code to review") + language: str = Field( + default="", description="Programming language hint, e.g. python, java, js" + ) + context: str = Field( + default="", max_length=2000, description="Optional task/context description" + ) + + +class ReviewIssue(BaseModel): + severity: Severity + category: Category + line: int | None = Field( + default=None, description="Approximate 1-based line number, null if unknown" + ) + title: str + description: str + suggestion: str + + +class ReviewReport(BaseModel): + summary: str = Field(..., description="One-paragraph overall summary") + score: int = Field(..., ge=0, le=100) + grade: str = Field(..., description="A/B/C/D derived from score") + issues: list[ReviewIssue] + strengths: list[str] + improvements: list[str] + + +class ReviewResponse(BaseModel): + ok: bool = True + language: str + model: str + report: ReviewReport + + +class HealthResponse(BaseModel): + status: str + commit: str + + +class VerificationResponse(BaseModel): + schemaVersion: int + slug: str + commit: str \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/render.yaml b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/render.yaml new file mode 100644 index 0000000..5126bec --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/render.yaml @@ -0,0 +1,18 @@ +services: + - type: web + name: code-review-agent + runtime: docker + plan: free + envVars: + - key: COMMIT + sync: false + - key: LLM_BASE_URL + value: https://api.deepseek.com/v1 + - key: LLM_MODEL + value: deepseek-chat + - key: LLM_API_KEY + sync: false + - key: LLM_TIMEOUT_SECONDS + value: "120" + healthCheckPath: /health + autoDeploy: true \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/requirements.txt b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/requirements.txt new file mode 100644 index 0000000..ba71658 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/requirements.txt @@ -0,0 +1,8 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +openai>=1.40.0 +httpx>=0.27.0 +pydantic>=2.7.0 +pydantic-settings>=2.3.0 +python-dotenv>=1.0.1 +fastmcp>=2.4.0 \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py new file mode 100644 index 0000000..5d70b51 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""Unit tests for the code review service.""" +import json + +import pytest +from fastapi.testclient import TestClient + +from app.config import get_settings +from app.main import app +from app.reviewer import _extract_json + +client = TestClient(app) + + +def test_health_returns_commit(): + resp = client.get("/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "ok" + assert body["commit"] == get_settings().commit + + +def test_verification_well_known(): + resp = client.get("/.well-known/xagent-verification.json") + assert resp.status_code == 200 + body = resp.json() + assert body["schemaVersion"] == 1 + assert body["slug"] == "kestarsheng-code-review-agent" + assert "commit" in body + + +def test_review_requires_body(): + resp = client.post("/v1/review", json={}) + assert resp.status_code == 422 + + +def test_review_validates_max_length(monkeypatch): + settings = get_settings() + monkeypatch.setattr(settings, "max_code_chars", 10) + resp = client.post( + "/v1/review", + json={"code": "x" * 100, "language": "python"}, + ) + assert resp.status_code == 413 + + +def test_review_reports_llm_error(monkeypatch): + from app import main as main_module + from app.reviewer import ReviewError + + def fake_review(*args, **kwargs): + raise ReviewError("LLM API Key 未配置") + + monkeypatch.setattr(main_module, "review_code", fake_review) + resp = client.post( + "/v1/review", + json={"code": "print(1)", "language": "python"}, + ) + assert resp.status_code == 502 + assert "LLM API Key 未配置" in resp.text + + +def test_extract_json_fenced(): + text = '```json\n{"issues": [], "score": 80, "summary": "ok"}\n```' + data = _extract_json(text) + assert data["score"] == 80 + + +def test_extract_json_embedded(): + text = '说明如下:{"issues": [], "score": 90, "summary": "good"} 结束' + data = _extract_json(text) + assert data["score"] == 90 + + +def test_extract_json_invalid(): + from app.reviewer import ReviewError + + with pytest.raises(ReviewError): + _extract_json("完全没有JSON") \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/vercel.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/vercel.json new file mode 100644 index 0000000..49aa8e6 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/vercel.json @@ -0,0 +1,17 @@ +{ + "services": { + "code-review-agent": { + "root": ".", + "entrypoint": "api/index.py" + } + }, + "rewrites": [ + { + "source": "/(.*)", + "destination": { + "type": "service", + "service": "code-review-agent" + } + } + ] +} diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html new file mode 100644 index 0000000..51a3bc9 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html @@ -0,0 +1,156 @@ + + + + + +Code Review Agent - AI 代码质量评审 + + + +
+

Code Review Agent

+

AI 代码质量评审服务 · 粘贴代码,秒出结构化评审报告

+
+
+
+
+ + +
+ + + +

+
+
+ +
尚未评审。点击「开始评审」调用 POST /v1/review。
+
+
+ + + \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json new file mode 100644 index 0000000..e63d7f6 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "name": "Code Review Agent", + "slug": "kestarsheng-code-review-agent", + "sourceRepository": "https://github.com/kestarsheng/code-review-agent", + "reviewCommit": "64b3365f0faad5b71f6710cd5dd0356c94262d74", + "apiBaseUrl": "https://code-review-agent-ashy-six.vercel.app/v1", + "healthCheckUrl": "https://code-review-agent-ashy-six.vercel.app/health", + "deploymentProofUrl": "https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json" +} \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md new file mode 100644 index 0000000..ef4a0c7 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md @@ -0,0 +1,83 @@ +# Verification evidence + +## Prerequisites + +- Review commit: `64b3365f0faad5b71f6710cd5dd0356c94262d74` +- API base URL: `https://code-review-agent-ashy-six.vercel.app/v1` +- Authentication: none + +## 1. Health check + +```bash +curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/health +``` + +Expected response: + +```json +{"status":"ok","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +``` + +## 2. Deployment proof + +```bash +curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json +``` + +Expected response: + +```json +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +``` + +## 3. Capability call + +```bash +curl --fail --silent --show-error \ + --request POST https://code-review-agent-ashy-six.vercel.app/v1/review \ + --header "content-type: application/json" \ + --data '{"code":"def f(x):\n return x / 0","language":"python"}' +``` + +Expected success response (abridged): + +```json +{ + "ok": true, + "language": "python", + "model": "deepseek-chat", + "report": { + "summary": "...", + "score": 30, + "grade": "D", + "issues": [ + { + "severity": "critical", + "category": "correctness", + "line": 2, + "title": "除零错误", + "description": "...", + "suggestion": "..." + } + ], + "strengths": ["..."], + "improvements": ["..."] + } +} +``` + +## 4. Safe error behavior + +Empty body: + +```bash +curl --fail --silent --show-error \ + --request POST https://code-review-agent-ashy-six.vercel.app/v1/review \ + --header "content-type: application/json" \ + --data '{}' +``` + +Expected: HTTP 422 with `{"ok":false,"error":...}`. + +Oversized code (> 60 000 chars): HTTP 413. +LLM provider failure: HTTP 502 with `{"ok":false,"error":"LLM 调用失败: ..."}`. \ No newline at end of file From 278397ec5f43ec0bccfe04cfd99f71a460319b9c Mon Sep 17 00:00:00 2001 From: kestarsheng Date: Thu, 10 Sep 2026 22:35:25 +0800 Subject: [PATCH 02/15] feat: upgrade to dual-engine code review (rules + LLM + cross-validation) - Rule engine: 22+ built-in rules across Python/JS/Java/Go - Diff parser: unified diff review for PR-level analysis - Dual-engine fusion: rules detect patterns, LLM adds semantics, cross-validation marks confirmed findings - New endpoints: POST /v1/review_diff, GET /v1/rules - MCP multi-tool: review_code, review_diff, detect_security, list_rules - Issue source attribution: rule / llm / confirmed - 24 tests, all passing - Review commit updated to d23b13b --- .../SUBMISSION.md | 10 +- .../source/app/diff_parser.py | 120 ++++++ .../source/app/main.py | 59 ++- .../source/app/mcp_server.py | 121 +++++- .../source/app/prompts.py | 66 +++- .../source/app/reviewer.py | 162 +++++++- .../source/app/rules_engine.py | 353 ++++++++++++++++++ .../source/app/schemas.py | 27 +- .../source/tests/test_app.py | 127 ++++++- .../source/web/index.html | 74 +++- .../submission.json | 2 +- .../verification/README.md | 6 +- 12 files changed, 1066 insertions(+), 61 deletions(-) create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md index cc35647..aea14e1 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -2,9 +2,9 @@ ## Capability -- **One-line description:** Review source code and return a structured quality report (correctness, security, performance, maintainability, best practices) so AI-generated code can be checked before merge. +- **One-line description:** Dual-engine code review: rule-based static analysis + LLM semantic review with cross-validation, returning a structured quality report so AI-generated code can be checked before merge. - **Who it helps:** Developers using AI coding tools (Claude Code, Codex, Cursor) and any AI Agent that needs a code-quality gate. -- **Capability boundary:** Accepts a single code snippet (up to 60 000 chars) plus optional language and context. Returns a JSON report with score (0-100), grade (A-D), issues, strengths and improvements. Does not execute, compile, or persist submitted code. Does not review entire repositories or run static-analysis binaries. +- **Capability boundary:** Accepts a single code snippet (up to 60 000 chars) or a unified diff, plus optional language and context. Returns a JSON report with score (0-100), grade (A-D), issues (with source attribution: rule/llm/confirmed), strengths and improvements. The rule engine covers 22+ built-in patterns across Python, JavaScript, Java, and Go. Does not execute, compile, or persist submitted code. ## Live API @@ -17,7 +17,7 @@ ## Source and reproducibility - **Source repository:** https://github.com/kestarsheng/code-review-agent -- **Review commit:** `64b3365f0faad5b71f6710cd5dd0356c94262d74` +- **Review commit:** `d23b13b0eff960d777ca724ed09b41db0dd94194` - **Source submitted in this PR:** `source/` - **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` - **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` @@ -28,12 +28,12 @@ The API must expose: ```json // GET /health -{"status":"ok","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +{"status":"ok","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} ``` ```json // GET /.well-known/xagent-verification.json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} ``` ## Verification diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py new file mode 100644 index 0000000..346eb0d --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/diff_parser.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +"""Unified diff parser for diff-level code review. + +Parses standard unified diff format (git diff, GitHub PR diff) and +extracts changed lines with context for targeted review. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass + + +@dataclass +class DiffHunk: + """A single hunk within a diff.""" + old_start: int + old_count: int + new_start: int + new_count: int + file_path: str + lines: list[tuple[str, str, int]] # (sign, content, new_line_number) + + +@dataclass +class ParsedDiff: + """Result of parsing a unified diff.""" + hunks: list[DiffHunk] + files_changed: list[str] + added_lines: int + removed_lines: int + reconstructed_code: str # changed lines joined for review + + +_HUNK_HEADER = re.compile( + r"^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@" +) +_FILE_HEADER = re.compile(r"^\+\+\+\s+(?:b/)?(.+)$") + + +def parse_diff(diff_text: str) -> ParsedDiff: + """Parse a unified diff string into structured hunks.""" + hunks: list[DiffHunk] = [] + files_changed: set[str] = set() + added = 0 + removed = 0 + + lines = diff_text.splitlines() + i = 0 + current_file = "" + reconstructed_parts: list[str] = [] + + while i < len(lines): + line = lines[i] + + file_match = _FILE_HEADER.match(line) + if file_match: + current_file = file_match.group(1) + files_changed.add(current_file) + i += 1 + continue + + hunk_match = _HUNK_HEADER.match(line) + if hunk_match: + old_start = int(hunk_match.group(1)) + old_count = int(hunk_match.group(2) or 1) + new_start = int(hunk_match.group(3)) + new_count = int(hunk_match.group(4) or 1) + + hunk_lines: list[tuple[str, str, int]] = [] + new_line_num = new_start + i += 1 + + while i < len(lines): + hl = lines[i] + if hl.startswith("@@") or hl.startswith("+++") or hl.startswith("---"): + break + + if hl.startswith("+"): + hunk_lines.append(("+", hl[1:], new_line_num)) + reconstructed_parts.append(hl[1:]) + new_line_num += 1 + added += 1 + elif hl.startswith("-"): + hunk_lines.append(("-", hl[1:], 0)) + removed += 1 + elif hl.startswith(" "): + hunk_lines.append((" ", hl[1:], new_line_num)) + new_line_num += 1 + elif hl == "": + pass + i += 1 + + hunks.append(DiffHunk( + old_start=old_start, old_count=old_count, + new_start=new_start, new_count=new_count, + file_path=current_file, + lines=hunk_lines, + )) + continue + + i += 1 + + reconstructed_code = "\n".join(reconstructed_parts) + return ParsedDiff( + hunks=hunks, + files_changed=sorted(files_changed), + added_lines=added, + removed_lines=removed, + reconstructed_code=reconstructed_code, + ) + + +def diff_summary(parsed: ParsedDiff) -> str: + """Generate a human-readable summary of the diff.""" + parts = [ + f"变更文件: {', '.join(parsed.files_changed) or '未知'}", + f"新增 {parsed.added_lines} 行, 删除 {parsed.removed_lines} 行", + f"共 {len(parsed.hunks)} 个 hunk", + ] + return " | ".join(parts) \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py index 8428539..d97c2ba 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py @@ -2,7 +2,9 @@ """FastAPI application entry point. Provides: -- POST /v1/review review source code and return a structured report +- POST /v1/review dual-engine review of source code +- POST /v1/review_diff dual-engine review of a unified diff +- GET /v1/rules list all built-in rule engine rules - GET /health health check returning the deployed commit - GET /.well-known/xagent-verification.json deployment proof - GET / minimal web demo page @@ -14,8 +16,11 @@ from fastapi.responses import HTMLResponse, JSONResponse from .config import PROJECT_SLUG, get_settings -from .reviewer import ReviewError, review_code +from .reviewer import ReviewError, review_code, review_diff +from .rules_engine import RULES, run_rules from .schemas import ( + DiffReviewRequest, + DiffReviewResponse, HealthResponse, ReviewRequest, ReviewResponse, @@ -28,9 +33,9 @@ settings = get_settings() app = FastAPI( title="Code Review Agent", - description="AI code quality review as a service. " - "Send code, get a structured review report.", - version="1.0.0", + description="Dual-engine AI code quality review: rule-based static analysis " + "+ LLM semantic review with cross-validation.", + version="2.0.0", ) @@ -64,6 +69,48 @@ async def review(req: ReviewRequest) -> ReviewResponse: return ReviewResponse(language=req.language, model=settings.llm_model, report=report) +@app.post("/v1/review_diff", response_model=DiffReviewResponse, tags=["review"]) +async def review_diff_endpoint(req: DiffReviewRequest) -> DiffReviewResponse: + if len(req.diff) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"diff 过长(限制 {settings.max_code_chars} 字符)", + ) + try: + result = review_diff(diff=req.diff, language=req.language, context=req.context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + diff_meta = result.get("diff_meta", {}) + report = {k: v for k, v in result.items() if k != "diff_meta"} + return DiffReviewResponse( + files_changed=diff_meta.get("files_changed", []), + added_lines=diff_meta.get("added_lines", 0), + removed_lines=diff_meta.get("removed_lines", 0), + model=settings.llm_model, + report=report, + ) + + +@app.get("/v1/rules", tags=["review"]) +def list_rules() -> dict: + """List all built-in rule engine rules.""" + return { + "total": len(RULES), + "rules": [ + { + "id": r.id, + "language": r.language, + "severity": r.severity, + "category": r.category, + "confidence": r.confidence, + "title": r.title, + } + for r in RULES + ], + } + + @app.get("/", response_class=HTMLResponse, include_in_schema=False) def index() -> HTMLResponse: html = Path(__file__).resolve().parent.parent / "web" / "index.html" @@ -87,4 +134,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py index bf05966..91efae2 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py @@ -1,33 +1,40 @@ # -*- coding: utf-8 -*- -"""FastMCP server exposing the code review capability as a reusable tool. +"""FastMCP server exposing code review capabilities as reusable MCP tools. -Run via stdio (default) for Claude Code / Codex / Cursor, or via -streamable HTTP transport on the same service. +Tools: +- review_code: dual-engine review of a source code snippet +- review_diff: dual-engine review of a unified diff / PR change +- detect_security: fast rule-only security scan (no LLM, instant) +- list_rules: list all built-in rule engine rules + +Run via stdio (default) for Claude Code / Codex / Cursor. """ import json from fastmcp import FastMCP from .config import PROJECT_SLUG -from .reviewer import ReviewError, review_code +from .reviewer import ReviewError, review_code, review_diff +from .rules_engine import RULES, run_rules mcp = FastMCP( PROJECT_SLUG, instructions=( - "Code quality review assistant. Review source code and return a " - "structured report covering correctness, security, performance, " - "maintainability and best practices." + "Dual-engine code review assistant. Combines a rule-based static " + "analysis engine with LLM semantic review for cross-validated " + "quality reports. Tools: review_code, review_diff, " + "detect_security, list_rules." ), ) @mcp.tool() -def review_code( +def review_code_tool( code: str, language: str = "", context: str = "", ) -> str: - """Review the given source code and return a structured quality report. + """Review source code with dual-engine (rules + LLM) and return a structured report. Args: code: source code to review. @@ -35,21 +42,105 @@ def review_code( context: optional description of what the code is supposed to do. Returns: - A JSON string with summary, score (0-100), grade (A-D), issues, - strengths and improvements. + JSON string with summary, score, grade, issues (with source attribution), + strengths, improvements, and engine_info. """ try: report = review_code(code=code, language=language, context=context) except ReviewError as exc: - return json.dumps( - {"ok": False, "error": str(exc)}, ensure_ascii=False - ) + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) return json.dumps({"ok": True, "report": report}, ensure_ascii=False) +@mcp.tool() +def review_diff_tool( + diff: str, + language: str = "", + context: str = "", +) -> str: + """Review a unified diff (e.g. git diff output) for change-level risks. + + Args: + diff: unified diff text. + language: programming language hint. + context: optional description of the change purpose. + + Returns: + JSON string with diff metadata and a structured review report. + """ + try: + result = review_diff(diff=diff, language=language, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + return json.dumps({"ok": True, "result": result}, ensure_ascii=False) + + +@mcp.tool() +def detect_security(code: str, language: str = "") -> str: + """Fast rule-only security scan — no LLM call, returns instantly. + + Args: + code: source code to scan. + language: programming language hint. + + Returns: + JSON string with detected security issues and their rule IDs. + """ + findings = run_rules(code, language) + security_findings = [ + f for f in findings if f.category in ("security", "ai_pattern") + ] + return json.dumps( + { + "ok": True, + "total_findings": len(security_findings), + "findings": [ + { + "rule_id": f.rule_id, + "severity": f.severity, + "category": f.category, + "line": f.line, + "title": f.title, + "description": f.description, + "suggestion": f.suggestion, + "confidence": f.confidence, + } + for f in security_findings + ], + }, + ensure_ascii=False, + ) + + +@mcp.tool() +def list_rules() -> str: + """List all built-in rule engine rules with their metadata. + + Returns: + JSON string with all rules (id, language, severity, category, title). + """ + return json.dumps( + { + "total": len(RULES), + "rules": [ + { + "id": r.id, + "language": r.language, + "severity": r.severity, + "category": r.category, + "confidence": r.confidence, + "title": r.title, + } + for r in RULES + ], + }, + ensure_ascii=False, + ) + + def main() -> None: mcp.run() if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py index 316d708..f25ded6 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Prompt templates for the code review engine.""" +"""Prompt templates for the dual-engine code review system.""" SYSTEM_PROMPT = """\ 你是一名资深软件架构师与代码评审专家,擅长从正确性、安全性、性能、\ @@ -7,6 +7,18 @@ 不得臆造。请以严格的 JSON 格式输出评审报告,不要输出任何 JSON 以外的内容。 """ +SYSTEM_PROMPT_WITH_RULES = """\ +你是一名资深软件架构师与代码评审专家。你正在与一个规则引擎协同工作——\ +规则引擎已通过静态模式匹配发现了若干已知问题(见下方"规则引擎预检结果")。\ +你的任务是: +1. 确认或否定规则引擎的发现(若规则误报请在 issues 中说明) +2. 发现规则引擎无法检测的语义问题(逻辑错误、架构缺陷、业务逻辑等) +3. 从正确性、安全性、性能、可维护性、最佳实践五个维度综合评审 + +你输出的评审结论必须基于代码事实,不得臆造。\ +请以严格的 JSON 格式输出评审报告,不要输出任何 JSON 以外的内容。 +""" + JSON_SCHEMA_EXAMPLE = """\ 评审报告 JSON 结构如下: { @@ -16,7 +28,7 @@ "issues": [ { "severity": "critical 或 major 或 minor 或 info", - "category": "correctness 或 security 或 performance 或 maintainability 或 best_practice", + "category": "correctness 或 security 或 performance 或 maintainability 或 best_practice 或 ai_pattern", "line": 问题所在的大致行号(1-based),无法确定时填 null, "title": "一句话问题标题", "description": "问题详细描述,说明为什么是问题、可能后果", @@ -31,6 +43,14 @@ 1. issues 中必须至少包含一条真正存在的问题;若确实没有问题,severity 用 info 说明代码状态优秀。 2. 不要编造代码中不存在的问题。security 类别优先于其他类别报告。 3. 建议要具体、可执行,需要时可附简短示例代码。 +4. 若规则引擎预检结果中存在误报,请在 issues 中用 severity=info 说明"规则引擎 XX 为误报"。 +""" + +DIFF_SYSTEM_PROMPT = """\ +你是一名资深代码评审专家,正在评审一个代码变更(diff)。\ +请重点关注变更部分的风险:是否引入安全漏洞、是否破坏现有逻辑、\ +是否有性能退化、变更是否完整(如新增分支但未处理所有路径)。\ +请以严格的 JSON 格式输出评审报告,不要输出任何 JSON 以外的内容。 """ @@ -41,4 +61,44 @@ def build_user_prompt(language: str, context: str, code: str) -> str: parts.append("待评审代码:") parts.append("```" + language + "\n" + code + "\n```") parts.append(JSON_SCHEMA_EXAMPLE) - return "\n".join(parts) \ No newline at end of file + return "\n".join(parts) + + +def build_user_prompt_with_rules( + language: str, + context: str, + code: str, + rule_findings: list[dict], +) -> str: + """Build prompt that includes rule engine pre-check results for LLM.""" + parts = [f"语言:{language or '未知'}"] + if context: + parts.append(f"任务上下文:{context}") + + if rule_findings: + parts.append("规则引擎预检结果:") + for f in rule_findings: + parts.append( + f" [{f['rule_id']}] {f['severity']}/{f['category']} " + f"行{f.get('line', '?')}: {f['title']}" + ) + parts.append("") + else: + parts.append("规则引擎预检结果:未发现已知模式问题。\n") + + parts.append("待评审代码:") + parts.append("```" + language + "\n" + code + "\n```") + parts.append(JSON_SCHEMA_EXAMPLE) + return "\n".join(parts) + + +def build_diff_prompt(language: str, context: str, diff: str, diff_meta: str) -> str: + """Build prompt for diff-level review.""" + parts = [f"语言:{language or '未知'}"] + if context: + parts.append(f"任务上下文:{context}") + parts.append(f"变更概要:{diff_meta}") + parts.append("待评审 diff:") + parts.append("```diff\n" + diff + "\n```") + parts.append(JSON_SCHEMA_EXAMPLE) + return "\n".join(parts) diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py index b169749..b156e6f 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py @@ -1,6 +1,15 @@ # -*- coding: utf-8 -*- -"""Core code review engine: calls an OpenAI-compatible LLM and returns a -structured JSON report.""" +"""Dual-engine code review: rule-based static analysis + LLM semantic review. + +The rule engine runs first (fast, deterministic, no API cost), detecting +known anti-patterns. The LLM then reviews the code with awareness of rule +findings, adding semantic analysis and confirming/denying rule hits. + +Results are merged with source attribution: +- "rule": found by rule engine only +- "llm": found by LLM only +- "confirmed": both engines agree (highest confidence) +""" import json import logging import re @@ -9,7 +18,15 @@ from openai import OpenAI from .config import get_settings -from .prompts import SYSTEM_PROMPT, build_user_prompt +from .diff_parser import diff_summary, parse_diff +from .prompts import ( + DIFF_SYSTEM_PROMPT, + SYSTEM_PROMPT_WITH_RULES, + build_diff_prompt, + build_user_prompt, + build_user_prompt_with_rules, +) +from .rules_engine import merge_findings, run_rules logger = logging.getLogger(__name__) @@ -33,11 +50,8 @@ def _extract_json(text: str) -> dict[str, Any]: return json.loads(match.group(0)) -def review_code(code: str, language: str = "", context: str = "") -> dict[str, Any]: - """Run the code review against the configured LLM. - - Returns the raw dict from the model after basic validation. - """ +def _call_llm(system_prompt: str, user_prompt: str) -> dict[str, Any]: + """Call the configured LLM and return parsed JSON.""" settings = get_settings() if not settings.llm_api_key: raise ReviewError("LLM API Key 未配置(环境变量 LLM_API_KEY)") @@ -47,13 +61,12 @@ def review_code(code: str, language: str = "", context: str = "") -> dict[str, A api_key=settings.llm_api_key, timeout=settings.llm_timeout_seconds, ) - user_prompt = build_user_prompt(language, context, code) try: resp = client.chat.completions.create( model=settings.llm_model, messages=[ - {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0.2, @@ -68,4 +81,131 @@ def review_code(code: str, language: str = "", context: str = "") -> dict[str, A if not isinstance(data, dict) or "issues" not in data: raise ReviewError("模型返回结构不完整,缺少 issues 字段") - return data \ No newline at end of file + return data + + +def review_code(code: str, language: str = "", context: str = "") -> dict[str, Any]: + """Run dual-engine review: rules first, then LLM with rule context. + + Returns a structured report with merged findings and engine metadata. + """ + # ── Phase 1: Rule engine (fast, local, no API cost) ── + rule_findings = run_rules(code, language) + rule_dicts = [ + { + "rule_id": f.rule_id, + "severity": f.severity, + "category": f.category, + "line": f.line, + "title": f.title, + "description": f.description, + "suggestion": f.suggestion, + "confidence": f.confidence, + } + for f in rule_findings + ] + logger.info("Rule engine found %d issues", len(rule_findings)) + + # ── Phase 2: LLM review with rule context ── + user_prompt = build_user_prompt_with_rules(language, context, code, rule_dicts) + llm_data = _call_llm(SYSTEM_PROMPT_WITH_RULES, user_prompt) + llm_issues = llm_data.get("issues", []) + + # ── Phase 3: Merge and attribute ── + merged_issues = merge_findings(rule_findings, llm_issues, code) + + rule_count = sum(1 for i in merged_issues if i.get("source") == "rule") + llm_count = sum(1 for i in merged_issues if i.get("source") == "llm") + confirmed_count = sum(1 for i in merged_issues if i.get("source") == "confirmed") + + report = { + "summary": llm_data.get("summary", ""), + "score": llm_data.get("score", 50), + "grade": llm_data.get("grade", "C"), + "issues": merged_issues, + "strengths": llm_data.get("strengths", []), + "improvements": llm_data.get("improvements", []), + "engine_info": { + "rule_count": rule_count, + "llm_count": llm_count, + "confirmed_count": confirmed_count, + "total_rules_run": len(rule_findings), + "engines": ["rule", "llm"], + }, + } + return report + + +def review_diff(diff: str, language: str = "", context: str = "") -> dict[str, Any]: + """Review a unified diff: parse, reconstruct changed code, dual-engine review.""" + parsed = parse_diff(diff) + if not parsed.reconstructed_code.strip(): + return { + "summary": "变更不包含实质性代码修改(仅删除或空白变更)。", + "score": 100, + "grade": "A", + "issues": [], + "strengths": ["变更无引入新代码的风险"], + "improvements": [], + "engine_info": { + "rule_count": 0, + "llm_count": 0, + "confirmed_count": 0, + "total_rules_run": 0, + "engines": ["rule", "llm"], + }, + "diff_meta": { + "files_changed": parsed.files_changed, + "added_lines": parsed.added_lines, + "removed_lines": parsed.removed_lines, + "hunks": len(parsed.hunks), + }, + } + + meta = diff_summary(parsed) + rule_findings = run_rules(parsed.reconstructed_code, language) + rule_dicts = [ + { + "rule_id": f.rule_id, + "severity": f.severity, + "category": f.category, + "line": f.line, + "title": f.title, + "description": f.description, + "suggestion": f.suggestion, + "confidence": f.confidence, + } + for f in rule_findings + ] + + user_prompt = build_diff_prompt(language, context, diff, meta) + llm_data = _call_llm(DIFF_SYSTEM_PROMPT, user_prompt) + llm_issues = llm_data.get("issues", []) + merged_issues = merge_findings(rule_findings, llm_issues, parsed.reconstructed_code) + + rule_count = sum(1 for i in merged_issues if i.get("source") == "rule") + llm_count = sum(1 for i in merged_issues if i.get("source") == "llm") + confirmed_count = sum(1 for i in merged_issues if i.get("source") == "confirmed") + + report = { + "summary": llm_data.get("summary", ""), + "score": llm_data.get("score", 50), + "grade": llm_data.get("grade", "C"), + "issues": merged_issues, + "strengths": llm_data.get("strengths", []), + "improvements": llm_data.get("improvements", []), + "engine_info": { + "rule_count": rule_count, + "llm_count": llm_count, + "confirmed_count": confirmed_count, + "total_rules_run": len(rule_findings), + "engines": ["rule", "llm"], + }, + "diff_meta": { + "files_changed": parsed.files_changed, + "added_lines": parsed.added_lines, + "removed_lines": parsed.removed_lines, + "hunks": len(parsed.hunks), + }, + } + return report diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py new file mode 100644 index 0000000..1bb7a25 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +"""Rule-based code analysis engine. + +Detects known anti-patterns, security risks, and AI-generated code +hallucinations through static pattern matching — no LLM required. + +Each rule has: +- id: stable identifier (e.g. PY-S001) +- language: target language (* = all) +- severity: critical / major / minor / info +- category: security / performance / maintainability / best_practice / ai_pattern +- pattern: compiled regex +- confidence: 0.0-1.0 (how likely this is a real issue) +- title / description / suggestion: human-readable guidance +""" +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Sequence + +@dataclass(frozen=True) +class Rule: + id: str + language: str + severity: str + category: str + pattern: re.Pattern + confidence: float + title: str + description: str + suggestion: str + +@dataclass +class Finding: + rule_id: str + severity: str + category: str + line: int + title: str + description: str + suggestion: str + confidence: float + source: str = "rule" + +def _r(rule_id, lang, sev, cat, pat, conf, title, desc, sug): + return Rule(rule_id, lang, sev, cat, re.compile(pat, re.MULTILINE), conf, title, desc, sug) + +RULES: list[Rule] = [ + + # ── Python: Security ────────────────────────────────────────── + _r("PY-S001", "python", "critical", "security", + r"\beval\s*\(", 0.95, + "使用 eval() 执行任意代码", + "eval() 会执行任意字符串作为代码,是严重的注入风险点。攻击者可通过构造恶意输入实现 RCE。", + "避免使用 eval()。如需解析表达式,使用 ast.literal_eval() 或专用解析器。"), + + _r("PY-S002", "python", "critical", "security", + r"\bexec\s*\(", 0.95, + "使用 exec() 执行任意代码", + "exec() 与 eval() 同样危险,可执行任意代码字符串,存在注入风险。", + "重构代码避免动态执行。如必须使用,确保输入经过严格沙箱过滤。"), + + _r("PY-S003", "python", "critical", "security", + r"(?:os\.system|subprocess\.call|subprocess\.run)\s*\(\s*['\"].*%s|" + r"(?:os\.system|subprocess\.(?:call|run|Popen))\s*\([^)]*shell\s*=\s*True", + 0.85, + "命令注入风险:shell=True 或字符串拼接执行系统命令", + "使用 shell=True 或字符串拼接构造命令时,攻击者可注入恶意命令参数。", + "使用 subprocess 并传入列表参数,设置 shell=False:\n" + "subprocess.run(['ls', user_input], shell=False)"), + + _r("PY-S004", "python", "major", "security", + r"(?:password|passwd|secret|api_key|token)\s*=\s*['\"][^'\"]{6,}['\"]", + 0.80, + "硬编码密钥/密码", + "代码中直接硬编码了密码、API Key 或 Token,泄露后可被直接利用。", + "使用环境变量或密钥管理服务:\n" + "api_key = os.environ['API_KEY']"), + + _r("PY-S005", "python", "major", "security", + r"pickle\.loads?\s*\(", + 0.90, + "使用 pickle 反序列化不可信数据", + "pickle.loads() 可执行任意代码,反序列化不可信数据等同于 RCE 漏洞。", + "使用 JSON 等安全格式序列化数据,或 jsonpickle 并限制类白名单。"), + + _r("PY-S006", "python", "major", "security", + r"\.execute\s*\([^)]*%[^)]|\.execute\s*\([^)]*\+|" + r"cursor\.execute\s*\(\s*f['\"]", + 0.85, + "SQL 注入风险:字符串拼接/f-string 构造 SQL", + "直接拼接变量到 SQL 语句中,攻击者可注入恶意 SQL 片段。", + "使用参数化查询:\n" + "cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))"), + + # ── Python: Performance ─────────────────────────────────────── + _r("PY-P001", "python", "major", "performance", + r"for\s+\w+\s+in\s+.*:\s*\n\s*for\s+\w+\s+in\s+.*:", + 0.70, + "嵌套循环可能导致 O(n²) 性能问题", + "双层嵌套循环在数据量大时性能急剧下降。", + "考虑使用集合/字典查找、itertools 或算法优化降低时间复杂度。"), + + _r("PY-P002", "python", "minor", "performance", + r"\.items\s*\(\)\s*\n\s*\w+\s*=\s*\w+\[0\]|\bfor\s+\w+\s+in\s+\w+\.keys\s*\(\):", + 0.60, + "遍历字典时未使用 .items() 导致多次查找", + "先遍历 keys 再用 dict[key] 访问值,每次查找都是 O(1) 但常数项大。", + "使用 for k, v in d.items() 一次获取键值。"), + + _r("PY-P003", "python", "minor", "performance", + r"list\s*\(\s*range\s*\(\s*\d{4,}\s*\)\s*\)", + 0.65, + "预生成大范围列表浪费内存", + "list(range(10000)) 会一次性分配所有元素内存。", + "直接使用 range() 迭代器,惰性求值节省内存。"), + + # ── Python: Best Practice ───────────────────────────────────── + _r("PY-B001", "python", "minor", "best_practice", + r"^\s*except\s*:|^\s*except\s+Exception\s*:", + 0.50, + "过宽的异常捕获", + "裸 except 或 except Exception 会吞掉所有异常,包括 KeyboardInterrupt、SystemExit。", + "捕获具体异常类型:\nexcept (ValueError, TypeError) as e:"), + + _r("PY-B002", "python", "info", "best_practice", + r"^\s*def\s+\w+\s*\([^)]*\)\s*:", + 0.40, + "函数缺少类型注解", + "无类型注解的函数可读性和可维护性较差,IDE 无法提供准确提示。", + "添加类型注解:\ndef add(a: int, b: int) -> int:"), + + # ── JavaScript: Security ────────────────────────────────────── + _r("JS-S001", "javascript", "critical", "security", + r"\beval\s*\(", + 0.95, + "使用 eval() 执行任意代码", + "JavaScript 中 eval() 执行任意字符串,是 XSS 和代码注入的主要入口。", + "使用 JSON.parse() 解析数据,或 Function 构造器配合严格输入验证。"), + + _r("JS-S002", "javascript", "critical", "security", + r"innerHTML\s*[=+]|document\.write\s*\(", + 0.85, + "innerHTML/document.write 导致 XSS", + "直接写入 innerHTML 或使用 document.write 会将字符串解析为 HTML,可注入恶意脚本。", + "使用 textContent 或 DOM API 创建元素:\n" + "el.textContent = userInput;"), + + _r("JS-S003", "javascript", "major", "security", + r"(?:password|secret|apiKey|token)\s*[:=]\s*['\"][^'\"]{6,}['\"]", + 0.80, + "硬编码密钥/Token", + "前端代码中的密钥可被任何用户查看,后端密钥泄露后可被直接利用。", + "使用环境变量(后端)或安全的配置服务,前端不存储敏感密钥。"), + + # ── JavaScript: AI Pattern ──────────────────────────────────── + _r("JS-A001", "javascript", "major", "ai_pattern", + r"import\s+.*\s+from\s+['\"](?:lodash-es|underscore)['\"]\s*;", + 0.55, + "AI 生成代码常见模式:全量导入 lodash", + "AI 常生成 import _ from 'lodash' 全量导入,现代前端应按需导入。", + "按需导入:import debounce from 'lodash/debounce'"), + + # ── Java: Security ──────────────────────────────────────────── + _r("JV-S001", "java", "critical", "security", + r"Runtime\.getRuntime\s*\(\s*\)\.exec\s*\(", + 0.90, + "使用 Runtime.exec() 执行命令", + "Runtime.exec() 若拼接用户输入可导致命令注入。", + "使用 ProcessBuilder 并传入参数列表,避免 shell 解释。"), + + _r("JV-S002", "java", "major", "security", + r"Statement\s+\w+\s*=.*\n.*\.execute\s*\(\s*['\"]\s*\+|" + r"createStatement\s*\(\s*\).*\n.*\.execute\s*\(\s*['\"]\s*\+", + 0.80, + "SQL 注入:Statement 拼接 SQL", + "使用 Statement 拼接 SQL 字符串存在注入风险。", + "使用 PreparedStatement 参数化查询:\n" + "ps = conn.prepareStatement(sql); ps.setString(1, input);"), + + # ── Go: Security ────────────────────────────────────────────── + _r("GO-S001", "go", "major", "security", + r"os\.Exec\s*\(", + 0.85, + "使用 os.Exec 执行外部命令", + "os.Exec 已被废弃,且拼接用户输入存在命令注入风险。", + "使用 exec.Command 并传入参数列表:\n" + "cmd := exec.Command('ls', arg)"), + + # ── Cross-language: AI Hallucination Patterns ───────────────── + _r("AI-H001", "*", "major", "ai_pattern", + r"(?:import|from|require)\s+['\"](?:react|vue|angular|svelte|next|nuxt)/" + r"(?:utils|helpers|common|shared)['\"]", + 0.45, + "可能引用了不存在的框架内部模块", + "AI 常臆造框架内部工具模块路径(如 react/utils),这些路径通常不存在。", + "检查该模块是否真实存在,使用框架官方文档确认正确导入路径。"), + + _r("AI-H002", "*", "minor", "ai_pattern", + r"(?:\.|->)\s*(?:forEach|map|filter|reduce)\s*\(\s*(?:async\s+)?\([^)]*\)\s*=>\s*\{[^}]*await\s+", + 0.55, + "在 forEach/map 中使用 await", + "Array.forEach 不等待 Promise,在回调中使用 await 会导致并发问题。map+await 同理。", + "使用 for...of 循环或 Promise.all:\n" + "await Promise.all(items.map(async fn))"), + + _r("AI-H003", "*", "major", "ai_pattern", + r"try\s*\{[^}]*\}\s*catch\s*\([^)]*\)\s*\{\s*(?:console\.log|print|System\.out)\s*\([^)]*\)\s*;?\s*\}", + 0.60, + "catch 块仅打印日志未处理异常", + "AI 常生成 catch (e) { console.log(e) } 模式,异常被吞掉且未上报。", + "至少应重新抛出或返回错误:\n" + "catch (e) { logger.error(e); throw e; }"), + + # ── Cross-language: Maintainability ─────────────────────────── + _r("X-M001", "*", "info", "maintainability", + r"(?:TODO|FIXME|HACK|XXX)\b", + 0.90, + "代码中存在 TODO/FIXME 标记", + "未完成的技术债务标记,应在发布前处理或转为 Issue 跟踪。", + "将 TODO 转为 Issue 跟踪,或在发布前完成修复。"), +] + +def detect_language(code: str, hint: str = "") -> str: + """Detect programming language from hint or code content.""" + hint = hint.lower().strip() + mapping = { + "py": "python", "python": "python", "python3": "python", + "js": "javascript", "javascript": "javascript", "jsx": "javascript", + "ts": "javascript", "typescript": "javascript", "tsx": "javascript", + "java": "java", "jsp": "java", + "go": "go", "golang": "go", + "c": "c", "cpp": "c", "c++": "c", "h": "c", + "rs": "rust", "rust": "rust", + "rb": "ruby", "ruby": "ruby", + "php": "php", + "sh": "shell", "bash": "shell", "shell": "shell", + } + if hint in mapping: + return mapping[hint] + + indicators = [ + (r"\bdef\s+\w+\s*\(", "python"), + (r"\bimport\s+java\.", "java"), + (r"\bpackage\s+\w+\s+import\s+", "go"), + (r"\bfunc\s+\w+\s*\(", "go"), + (r"\b(?:const|let|var)\s+\w+\s*=", "javascript"), + (r"\bfn\s+\w+\s*\(", "rust"), + (r"\bdef\s+\w+\s*$", "ruby"), + (r"<\?php", "php"), + (r"#!/bin/(?:ba)?sh", "shell"), + ] + for pattern, lang in indicators: + if re.search(pattern, code): + return lang + return "" + +def _line_number(code: str, pos: int) -> int: + return code.count("\n", 0, pos) + 1 + +def run_rules(code: str, language: str = "") -> list[Finding]: + """Run all applicable rules against the code and return findings.""" + detected = detect_language(code, language) + + findings: list[Finding] = [] + for rule in RULES: + if rule.language != "*" and rule.language != detected: + continue + for match in rule.pattern.finditer(code): + findings.append(Finding( + rule_id=rule.id, + severity=rule.severity, + category=rule.category, + line=_line_number(code, match.start()), + title=rule.title, + description=rule.description, + suggestion=rule.suggestion, + confidence=rule.confidence, + source="rule", + )) + return findings + +def merge_findings( + rule_findings: list[Finding], + llm_issues: list[dict], + code: str, +) -> list[dict]: + """Merge rule-based findings with LLM-detected issues. + + Strategy: + - Rule findings with confidence >= 0.8 are kept as-is (high trust) + - Rule findings with confidence < 0.8 are marked for LLM confirmation + - LLM issues that overlap with rule findings are upgraded (confirmed) + - LLM-only issues are kept with source="llm" + - Deduplicate by (line, category) proximity + """ + merged: list[dict] = [] + seen_positions: set[tuple[str, int]] = set() + + for f in rule_findings: + key = (f.category, f.line) + if key in seen_positions: + continue + seen_positions.add(key) + merged.append({ + "severity": f.severity, + "category": f.category, + "line": f.line, + "title": f.title, + "description": f.description, + "suggestion": f.suggestion, + "source": "rule", + "rule_id": f.rule_id, + "confidence": f.confidence, + }) + + for issue in llm_issues: + line = issue.get("line") + cat = issue.get("category", "") + key = (cat, line or 0) + + overlap = any( + abs((m.get("line") or 0) - (line or 0)) <= 2 + and m.get("category") == cat + for m in merged + ) + + if overlap: + for m in merged: + if (abs((m.get("line") or 0) - (line or 0)) <= 2 + and m.get("category") == cat): + m["source"] = "confirmed" + m["confidence"] = min(1.0, m.get("confidence", 0.5) + 0.3) + if not m.get("description") and issue.get("description"): + m["description"] = issue["description"] + break + else: + merged.append({ + "severity": issue.get("severity", "info"), + "category": cat, + "line": line, + "title": issue.get("title", ""), + "description": issue.get("description", ""), + "suggestion": issue.get("suggestion", ""), + "source": "llm", + "rule_id": None, + "confidence": 0.7, + }) + + severity_order = {"critical": 0, "major": 1, "minor": 2, "info": 3} + merged.sort(key=lambda x: (severity_order.get(x["severity"], 9), x.get("line") or 0)) + return merged \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py index b2dfcf8..a8155ef 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py @@ -6,8 +6,9 @@ Severity = Literal["critical", "major", "minor", "info"] Category = Literal[ - "correctness", "security", "performance", "maintainability", "best_practice" + "correctness", "security", "performance", "maintainability", "best_practice", "ai_pattern" ] +IssueSource = Literal["rule", "llm", "confirmed"] class ReviewRequest(BaseModel): @@ -20,6 +21,12 @@ class ReviewRequest(BaseModel): ) +class DiffReviewRequest(BaseModel): + diff: str = Field(..., min_length=1, description="Unified diff text to review") + language: str = Field(default="", description="Programming language hint") + context: str = Field(default="", max_length=2000, description="Optional context") + + class ReviewIssue(BaseModel): severity: Severity category: Category @@ -29,6 +36,9 @@ class ReviewIssue(BaseModel): title: str description: str suggestion: str + source: IssueSource = Field(default="llm", description="Which engine found this issue") + rule_id: str | None = Field(default=None, description="Rule ID if from rule engine") + confidence: float = Field(default=0.7, ge=0.0, le=1.0, description="Confidence score") class ReviewReport(BaseModel): @@ -38,6 +48,10 @@ class ReviewReport(BaseModel): issues: list[ReviewIssue] strengths: list[str] improvements: list[str] + engine_info: dict = Field( + default_factory=dict, + description="Engine metadata: rule_count, llm_count, confirmed_count, languages", + ) class ReviewResponse(BaseModel): @@ -47,6 +61,15 @@ class ReviewResponse(BaseModel): report: ReviewReport +class DiffReviewResponse(BaseModel): + ok: bool = True + files_changed: list[str] + added_lines: int + removed_lines: int + model: str + report: ReviewReport + + class HealthResponse(BaseModel): status: str commit: str @@ -55,4 +78,4 @@ class HealthResponse(BaseModel): class VerificationResponse(BaseModel): schemaVersion: int slug: str - commit: str \ No newline at end of file + commit: str diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py index 5d70b51..695a7f6 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py @@ -1,17 +1,21 @@ # -*- coding: utf-8 -*- -"""Unit tests for the code review service.""" +"""Unit tests for the dual-engine code review service.""" import json import pytest from fastapi.testclient import TestClient from app.config import get_settings +from app.diff_parser import parse_diff from app.main import app from app.reviewer import _extract_json +from app.rules_engine import detect_language, merge_findings, run_rules client = TestClient(app) +# ── Meta endpoints ────────────────────────────────────────────── + def test_health_returns_commit(): resp = client.get("/health") assert resp.status_code == 200 @@ -29,6 +33,118 @@ def test_verification_well_known(): assert "commit" in body +# ── Rule engine ───────────────────────────────────────────────── + +def test_rules_detect_eval(): + code = "result = eval(user_input)" + findings = run_rules(code, "python") + ids = [f.rule_id for f in findings] + assert "PY-S001" in ids + + +def test_rules_detect_hardcoded_secret(): + code = 'api_key = "sk-1234567890abcdef"' + findings = run_rules(code, "python") + ids = [f.rule_id for f in findings] + assert "PY-S004" in ids + + +def test_rules_detect_sql_injection(): + code = 'cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")' + findings = run_rules(code, "python") + ids = [f.rule_id for f in findings] + assert "PY-S006" in ids + + +def test_rules_detect_js_innerhtml(): + code = "document.getElementById('x').innerHTML = userInput" + findings = run_rules(code, "javascript") + ids = [f.rule_id for f in findings] + assert "JS-S002" in ids + + +def test_rules_detect_todo(): + code = "# TODO: fix this later\npass" + findings = run_rules(code, "python") + ids = [f.rule_id for f in findings] + assert "X-M001" in ids + + +def test_rules_clean_code_no_findings(): + code = "def add(a: int, b: int) -> int:\n return a + b" + findings = run_rules(code, "python") + security = [f for f in findings if f.severity == "critical"] + assert len(security) == 0 + + +def test_detect_language_python(): + assert detect_language("def foo():\n pass", "python") == "python" + assert detect_language("def foo():\n pass", "") == "python" + + +def test_detect_language_javascript(): + assert detect_language("const x = 1", "js") == "javascript" + assert detect_language("const x = 1", "") == "javascript" + + +def test_detect_language_java(): + assert detect_language("import java.util.List;", "") == "java" + + +def test_detect_language_go(): + assert detect_language("func main() {\n}", "") == "go" + + +def test_merge_findings_rule_only(): + rule_findings = run_rules("eval('1+1')", "python") + merged = merge_findings(rule_findings, [], "eval('1+1')") + assert any(m["source"] == "rule" for m in merged) + + +def test_merge_findings_confirmed(): + rule_findings = run_rules("eval('1+1')", "python") + llm_issues = [ + {"severity": "critical", "category": "security", "line": 1, + "title": "eval", "description": "RCE", "suggestion": "don't use eval"} + ] + merged = merge_findings(rule_findings, llm_issues, "eval('1+1')") + assert any(m["source"] == "confirmed" for m in merged) + + +def test_list_rules_endpoint(): + resp = client.get("/v1/rules") + assert resp.status_code == 200 + body = resp.json() + assert body["total"] > 10 + assert any(r["id"] == "PY-S001" for r in body["rules"]) + + +# ── Diff parser ───────────────────────────────────────────────── + +def test_parse_simple_diff(): + diff = """--- a/foo.py ++++ b/foo.py +@@ -1,3 +1,4 @@ + def foo(): +- return 1 ++ return 2 ++ pass +""" + parsed = parse_diff(diff) + assert "foo.py" in parsed.files_changed + assert parsed.added_lines == 2 + assert parsed.removed_lines == 1 + assert len(parsed.hunks) == 1 + + +def test_parse_empty_diff(): + parsed = parse_diff("") + assert len(parsed.hunks) == 0 + assert parsed.added_lines == 0 + + +# ── Review API ────────────────────────────────────────────────── + def test_review_requires_body(): resp = client.post("/v1/review", json={}) assert resp.status_code == 422 @@ -60,6 +176,13 @@ def fake_review(*args, **kwargs): assert "LLM API Key 未配置" in resp.text +def test_review_diff_requires_body(): + resp = client.post("/v1/review_diff", json={}) + assert resp.status_code == 422 + + +# ── JSON extraction ───────────────────────────────────────────── + def test_extract_json_fenced(): text = '```json\n{"issues": [], "score": 80, "summary": "ok"}\n```' data = _extract_json(text) @@ -76,4 +199,4 @@ def test_extract_json_invalid(): from app.reviewer import ReviewError with pytest.raises(ReviewError): - _extract_json("完全没有JSON") \ No newline at end of file + _extract_json("完全没有JSON") diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html index 51a3bc9..556b6e4 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html @@ -3,7 +3,7 @@ -Code Review Agent - AI 代码质量评审 +Code Review Agent - 双引擎 AI 代码评审

Code Review Agent

-

AI 代码质量评审服务 · 粘贴代码,秒出结构化评审报告

+

双引擎 AI 代码评审 · 规则引擎 + LLM 语义分析 · 交叉验证降低误报

+
+
代码评审
+
Diff 评审
+
- + @@ -80,16 +94,34 @@

Code Review Agent

-
尚未评审。点击「开始评审」调用 POST /v1/review。
+
尚未评审。点击「开始评审」调用双引擎评审。
- \ No newline at end of file + diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json index e63d7f6..3364c2d 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json @@ -3,7 +3,7 @@ "name": "Code Review Agent", "slug": "kestarsheng-code-review-agent", "sourceRepository": "https://github.com/kestarsheng/code-review-agent", - "reviewCommit": "64b3365f0faad5b71f6710cd5dd0356c94262d74", + "reviewCommit": "d23b13b0eff960d777ca724ed09b41db0dd94194", "apiBaseUrl": "https://code-review-agent-ashy-six.vercel.app/v1", "healthCheckUrl": "https://code-review-agent-ashy-six.vercel.app/health", "deploymentProofUrl": "https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json" diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md index ef4a0c7..9ae28c1 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- Review commit: `64b3365f0faad5b71f6710cd5dd0356c94262d74` +- Review commit: `d23b13b0eff960d777ca724ed09b41db0dd94194` - API base URL: `https://code-review-agent-ashy-six.vercel.app/v1` - Authentication: none @@ -15,7 +15,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"status":"ok","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +{"status":"ok","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} ``` ## 2. Deployment proof @@ -27,7 +27,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"64b3365f0faad5b71f6710cd5dd0356c94262d74"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} ``` ## 3. Capability call From 37568b1c63a52995880a1a958afb59056a77505f Mon Sep 17 00:00:00 2001 From: kestarsheng Date: Fri, 11 Sep 2026 11:49:08 +0800 Subject: [PATCH 03/15] feat: update submission with latest features (CLI, dimension scores, fix_code, product landing page) - Sync source/ to commit 13a336d (latest main) - Add cli.py for one-click git diff review - Update submission.json reviewCommit to 13a336d - Update SUBMISSION.md with multi-file, MCP, CLI, dimension scores - Update verification/README.md commit hash --- .../SUBMISSION.md | 10 +- .../source/README.md | 224 +++++- .../source/app/main.py | 94 ++- .../source/app/mcp_server.py | 170 ++++- .../source/app/prompts.py | 110 ++- .../source/app/reviewer.py | 308 ++++++++- .../source/app/rules_engine.py | 80 +++ .../source/app/schemas.py | 43 ++ .../source/cli.py | 171 +++++ .../source/tests/test_app.py | 424 ++++++++++++ .../source/web/index.html | 645 +++++++++++++----- .../submission.json | 2 +- .../verification/README.md | 6 +- 13 files changed, 2057 insertions(+), 230 deletions(-) create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md index aea14e1..48cae19 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -4,7 +4,7 @@ - **One-line description:** Dual-engine code review: rule-based static analysis + LLM semantic review with cross-validation, returning a structured quality report so AI-generated code can be checked before merge. - **Who it helps:** Developers using AI coding tools (Claude Code, Codex, Cursor) and any AI Agent that needs a code-quality gate. -- **Capability boundary:** Accepts a single code snippet (up to 60 000 chars) or a unified diff, plus optional language and context. Returns a JSON report with score (0-100), grade (A-D), issues (with source attribution: rule/llm/confirmed), strengths and improvements. The rule engine covers 22+ built-in patterns across Python, JavaScript, Java, and Go. Does not execute, compile, or persist submitted code. +- **Capability boundary:** Accepts a single code snippet (up to 60 000 chars), a unified diff, or multiple files (structured list), plus optional language and context. Returns a JSON report with score (0-100), grade (A-D), five-dimension scores (correctness, security, performance, maintainability, best_practice), issues (with source attribution: rule/llm/confirmed, fix_code), strengths and improvements. The rule engine covers 22+ built-in patterns across Python, JavaScript, Java, and Go. Also provides MCP tools (7 total) and a CLI for git-diff review. Does not execute, compile, or persist submitted code. ## Live API @@ -12,12 +12,12 @@ - **Health-check URL:** https://code-review-agent-ashy-six.vercel.app/health - **Authentication:** none - **Rate limits / known limits:** Single request limited by LLM provider timeout (120 s). Max code size 60 000 chars. Free-tier hosting may cold-start. -- **API contract:** OpenAPI at `/docs`; request `POST /v1/review` body `{"code": string, "language"?: string, "context"?: string}`, response `{"ok": true, "language": string, "model": string, "report": ReviewReport}`. +- **API contract:** OpenAPI at `/docs`; request `POST /v1/review` body `{"code": string, "language"?: string, "context"?: string}`, response `{"ok": true, "language": string, "model": string, "report": ReviewReport}`. Also supports `POST /v1/review_diff` (unified diff input) and `POST /v1/review_files` (multi-file batch review). MCP endpoint at `/mcp` with 7 tools. ## Source and reproducibility - **Source repository:** https://github.com/kestarsheng/code-review-agent -- **Review commit:** `d23b13b0eff960d777ca724ed09b41db0dd94194` +- **Review commit:** `13a336d2b8242bfff8fc0655022d71493a4e4a23` - **Source submitted in this PR:** `source/` - **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` - **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` @@ -28,12 +28,12 @@ The API must expose: ```json // GET /health -{"status":"ok","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} +{"status":"ok","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} ``` ```json // GET /.well-known/xagent-verification.json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} ``` ## Verification diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md index c877903..44d8080 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md @@ -1,16 +1,76 @@ # Code Review Agent -AI 代码质量评审服务(Code Review as a Service)。把代码送给 LLM,返回结构化质量报告,亦可作为 MCP 工具被 Claude Code / Codex / Cursor 等 Agent 调用。 +**双引擎 AI 代码质量评审服务**(Code Review as a Service)。规则引擎 + LLM 语义分析 + 交叉验证,输出带分维度评分和可直接应用修复代码的结构化报告。提供 REST API 与 MCP 工具,可被 Claude Code / Codex / Cursor 等 Agent 直接调用。 > Submission for **X-Agent AI MCP Hackathon 2026 · Open Innovation Challenge**. +> +> 在线演示:https://code-review-agent-ashy-six.vercel.app -## What it does +## 双引擎架构 -- `POST /v1/review` — 提交代码片段,返回结构化评审报告(正确性 / 安全 / 性能 / 可维护性 / 最佳实践) -- `GET /health` — 健康检查,返回当前部署 Commit -- `GET /.well-known/xagent-verification.json` — 部署证明(slug + commit) -- `GET /` — 在线演示页(粘贴代码即时出报告) -- MCP 工具 `review_code` — 供 Agent 调用(stdio / streamable HTTP) +``` +┌─────────────────────────────────────────────────────────┐ +│ 输入:代码 / Diff / 多文件 │ +└───────────────┬─────────────────────────────────────────┘ + ▼ +┌──────────────────────────┐ ┌─────────────────────────────┐ +│ ① 规则引擎(确定性) │ │ ② LLM 语义分析(深度) │ +│ · 22 条跨语言规则 │ │ · 显式接收规则预检结果 │ +│ · Python/JS/Java/Go/C* │───▶ · 确认/否定规则命中(去误报) │ +│ · 安全/性能/AI幻觉/风格 │ │ · 发现语义级问题(逻辑/架构) │ +│ · 零成本、毫秒级、离线可跑 │ │ · 生成分维度评分与 fix_code │ +└───────────────┬──────────┘ └──────────────┬──────────────┘ + ▼ ▼ +┌───────────────────────────────────────────────────────────┐ +│ ③ 交叉验证合并(merge_findings) │ +│ · rule — 仅规则引擎命中(高置信保留) │ +│ · llm — 仅 LLM 发现 │ +│ · confirmed — 双引擎一致(置信度提升 +0.3,最高 1.0) │ +└───────────────────────────────┬───────────────────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ ④ 输出:五维度评分 + 可应用修复 + 引擎溯源 │ +│ · correctness/security/performance/maintainability/best_practice │ +│ · score = 加权平均(security 30% · correctness 25%) │ +│ · 每个 issue 附带 fix_code(可直接复制替换) │ +└───────────────────────────────────────────────────────────┘ +``` + +## 功能特性 + +- **双引擎评审** — 规则引擎先做确定性静态扫描,LLM 带规则上下文语义评审,交叉验证降低误报 +- **五维度评分** — 正确性 / 安全性 / 性能 / 可维护性 / 最佳实践各一个 0–100 分,加权得综合分 +- **可直接应用的修复代码** — 规则引擎为 8 类关键规则自动生成 `fix_code`,LLM 覆盖更复杂的修复 +- **三种评审模式** — 单文件代码、Unified Diff(PR 变更)、多文件批量(跨文件架构问题) +- **CLI 一键评审** — `python cli.py` 直接读 git diff 评审,无需粘贴代码 +- **MCP 工具集** — 7 个工具:评审 / Diff 评审 / 多文件评审 / 安全扫描 / 规则解释 / 修复生成 / 规则列表 +- **交互式演示页** — 暗色模式、代码高亮、维度评分条、引擎可视化、"一键应用修复" + +## CLI 一键评审(推荐) + +```bash +python cli.py # 评审工作区未提交改动 (git diff) +python cli.py --staged # 评审已暂存改动 (git diff --cached) +python cli.py --commit HEAD~1 # 评审最近一次提交 +python cli.py src/utils.py # 评审单个文件 +python cli.py --remote # 用远程 Vercel 部署(无需启动本地服务) +``` + +自动读取 git diff → 调 API → 输出带严重度图标、维度评分、修复代码的结构化报告。 + +## API 一览 + +| Method | Endpoint | 说明 | +| --- | --- | --- | +| `POST` | `/v1/review` | 评审源代码,返回结构化报告 | +| `POST` | `/v1/review_diff` | 评审 Unified Diff(PR 变更) | +| `POST` | `/v1/review_files` | 多文件批量评审(跨文件架构分析) | +| `POST` | `/v1/suggest_fix` | 为问题代码生成完整修复版本 | +| `GET` | `/v1/rules` | 列出全部规则引擎规则 | +| `GET` | `/v1/rules/{rule_id}` | 查看单条规则详情与修复指引 | +| `GET` | `/health` | 健康检查,返回部署 Commit | +| `GET` | `/.well-known/xagent-verification.json` | 部署证明(slug + commit) | +| `GET` | `/` | 在线演示页 | ## Quick start (local) @@ -18,27 +78,86 @@ AI 代码质量评审服务(Code Review as a Service)。把代码送给 LLM python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt -cp .env.example .env # 填入 LLM_API_KEY 等 +cp .env.example .env # 填入 LLM_API_KEY uvicorn app.main:app --reload ``` -Open http://127.0.0.1:8000 (demo page) or http://127.0.0.1:8000/docs (Swagger). +打开 http://127.0.0.1:8000 使用演示页,或 http://127.0.0.1:8000/docs 查看 Swagger。 -Example call: +### 示例:评审一段代码 ```bash curl -X POST http://127.0.0.1:8000/v1/review \ -H "Content-Type: application/json" \ - -d '{"code": "def foo(a, b):\n return a / b", "language": "python"}' + -d '{"code": "result = eval(user_input)", "language": "python"}' +``` + +响应(节选): + +```json +{ + "report": { + "score": 68, + "grade": "C", + "dimension_scores": { + "correctness": 88, "security": 35, + "performance": 90, "maintainability": 80, "best_practice": 75 + }, + "issues": [ + { + "severity": "critical", + "category": "security", + "line": 1, + "title": "使用 eval() 执行任意代码", + "description": "eval() 会执行任意字符串作为代码,是严重的注入风险点。", + "suggestion": "使用 ast.literal_eval() 或专用解析器。", + "fix_code": "result = ast.literal_eval(user_input)", + "source": "confirmed", + "rule_id": "PY-S001", + "confidence": 1.0 + } + ], + "engine_info": { + "rule_count": 0, "llm_count": 0, "confirmed_count": 1, + "total_rules_run": 3, "engines": ["rule", "llm"] + } + } +} +``` + +### 示例:评审一个 Diff + +```bash +curl -X POST http://127.0.0.1:8000/v1/review_diff \ + -H "Content-Type: application/json" \ + -d '{"diff": "--- a/x.py\n+++ b/x.py\n@@ -1,3 +1,4 @@\n def f():\n- return 1\n+ return eval(data)", "language": "python"}' +``` + +响应会包含 `files_changed` / `added_lines` / `removed_lines` 变更元数据与完整报告。 + +### 示例:多文件评审 + +```json +{ + "context": "用户服务模块", + "files": [ + {"filename": "utils.py", "content": "import os\napi_key = os.environ['KEY']", "language": "python"}, + {"filename": "main.py", "content": "from utils import *\nresult = eval(req.body)", "language": "python"} + ] +} ``` +返回每个文件的 `file_reports`(规则扫描)与一个 `overall_report`(LLM 跨文件架构评审)。 + ## MCP usage +### 本地 stdio(Claude Code / Codex / Cursor) + ```bash -python -m app.mcp_server # stdio transport for Claude Code / Codex / Cursor +python -m app.mcp_server # stdio transport ``` -Or register in your client config: +注册到客户端配置: ```json { @@ -51,28 +170,89 @@ Or register in your client config: } ``` -## Configuration (env vars) +### 远程 streamable HTTP(同一部署,无需本地 Python) + +部署后访问 `https:///mcp`,在 MCP 客户端中配置: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "npx", + "args": ["-y", "@anthropic-ai/mcp-client", "https:///mcp"] + } + } +} +``` + +> 远程 MCP 端点与 REST API 共用同一个服务器,部署后 `/mcp` 提供 streamable HTTP 协议,`/v1/*` 提供 REST。 + +### 使用引导(给 Agent) + +1. **先免费快筛**:用 `detect_security` / `list_rules` / `explain_issue`(无 LLM 调用,毫秒级返回) +2. **深度评审**:用 `review_code` / `review_diff` / `review_files`,默认 `detail="brief"`(节省上下文,仅返回标题级 issue) +3. **需要完整报告时**:`detail="full"` 返回每个 issue 的完整 description / suggestion / fix_code +4. **修复**:用 `suggest_fix` 获取可直接替换的 `fixed_code` + +### MCP 工具 + +| 工具 | 参数 | LLM | 说明 | +| --- | --- | --- | --- | +| `review_code` | `code, language?, context?, detail?` | ✅ | 评审源代码(`detail: "brief"\|"full"`) | +| `review_diff` | `diff, language?, context?, detail?` | ✅ | 评审 Unified Diff | +| `review_files` | `files: [{filename, content, language?}], context?, detail?` | ✅ | 多文件批量评审(结构化参数,非 JSON 字符串) | +| `detect_security` | `code, language?` | ❌ | 仅规则引擎安全扫描,即时返回 | +| `explain_issue` | `rule_id` | ❌ | 解释某条规则(定义/严重级别/修复指引) | +| `suggest_fix` | `code, language?, context?` | ✅ | 返回修复后的完整代码(fixed_code + 变更说明) | +| `list_rules` | — | ❌ | 列出全部规则 | + +> `review_files` 的 `files` 参数是**结构化数组**,每个元素 `{filename, content, language?}`,Agent 无需手工拼 JSON 字符串。 + +## 规则引擎 + +内置 **22 条跨语言规则**,覆盖 Python / JavaScript / Java / Go / C* / 跨语言通用模式: + +| 类别 | 示例 | +| --- | --- | +| Security | `eval`/`exec`、SQL 注入、命令注入、硬编码密钥、`pickle.loads`、`innerHTML` XSS | +| Performance | 嵌套循环 O(n²)、字典遍历未用 `.items()`、预生成大列表 | +| AI Pattern | 幻觉导入框架内部模块、`forEach` 中 `await`、catch 吞异常 | +| Maintainability / Best Practice | TODO/FIXME、裸 `except`、缺类型注解 | + +8 类关键规则带 **自动修复代码生成**(`eval`→`ast.literal_eval`、`innerHTML`→`textContent`、硬编码密钥→`os.environ` 等)。 + +## 配置(环境变量) | Var | Default | Description | | --- | --- | --- | | `LLM_BASE_URL` | `https://api.deepseek.com/v1` | OpenAI-compatible base URL | -| `LLM_API_KEY` | — | API key (required) | -| `LLM_MODEL` | `deepseek-chat` | Model name | -| `LLM_TIMEOUT_SECONDS` | `120` | LLM request timeout | -| `COMMIT` | `dev` | Deployed commit, returned by /health and /.well-known/xagent-verification.json | +| `LLM_API_KEY` | — | API key(必填) | +| `LLM_MODEL` | `deepseek-chat` | 模型名 | +| `LLM_TIMEOUT_SECONDS` | `120` | LLM 请求超时 | +| `MAX_CODE_CHARS` | `60000` | 单次评审最大字符数 | +| `COMMIT` | `dev` | 部署 Commit,/health 与验证文件返回 | -## Deployment +## 部署 -- Docker: `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent` -- Render: push to repo, set env vars, use `render.yaml` — set `COMMIT` to the exact deployed commit. +- **Vercel**(当前):`vercel.json` 已配置 Serverless 服务;在 Vercel 项目设置环境变量后推送即可 +- **Docker**: `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent` +- **Render**: 使用 `render.yaml`,推送仓库并设置环境变量 -After deploy, verify: +部署后验证: ```bash curl https:///health curl https:///.well-known/xagent-verification.json ``` +## 测试 + +```bash +python -m pytest tests/ -v +``` + +48 个单元测试,覆盖规则引擎、Diff 解析、五维度评分、修复代码生成、多文件评审与完整双引擎流程。 + ## License UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py index d97c2ba..e8d157c 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/main.py @@ -4,23 +4,38 @@ Provides: - POST /v1/review dual-engine review of source code - POST /v1/review_diff dual-engine review of a unified diff +- POST /v1/review_files multi-file batch review +- POST /v1/suggest_fix generate corrected code for known issues - GET /v1/rules list all built-in rule engine rules +- GET /v1/rules/{rule_id} explain one rule in detail - GET /health health check returning the deployed commit - GET /.well-known/xagent-verification.json deployment proof - GET / minimal web demo page +- MCP /mcp remote streamable-HTTP MCP endpoint (same server) """ import logging from pathlib import Path from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse from .config import PROJECT_SLUG, get_settings -from .reviewer import ReviewError, review_code, review_diff +from .mcp_server import mcp +from .reviewer import ( + ReviewError, + explain_issue, + review_code, + review_diff, + review_files, + suggest_fix_for_code, +) from .rules_engine import RULES, run_rules from .schemas import ( DiffReviewRequest, DiffReviewResponse, + FilesReviewRequest, + FilesReviewResponse, HealthResponse, ReviewRequest, ReviewResponse, @@ -31,14 +46,33 @@ logger = logging.getLogger(__name__) settings = get_settings() + +# Remote MCP endpoint (streamable HTTP transport). Created before app so its +# lifespan can be wired into the FastAPI app; mounted after all REST routes. +mcp_app = None +try: + mcp_app = mcp.http_app(path="/mcp") + logger.info("MCP streamable HTTP endpoint ready at /mcp") +except Exception: # noqa: BLE001 + logger.exception("Failed to build MCP HTTP endpoint") + app = FastAPI( title="Code Review Agent", description="Dual-engine AI code quality review: rule-based static analysis " "+ LLM semantic review with cross-validation.", - version="2.0.0", + version="2.1.0", + lifespan=mcp_app.lifespan if mcp_app is not None else None, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], ) + @app.get("/health", response_model=HealthResponse, tags=["meta"]) def health() -> HealthResponse: return HealthResponse(status="ok", commit=settings.commit) @@ -92,6 +126,31 @@ async def review_diff_endpoint(req: DiffReviewRequest) -> DiffReviewResponse: ) +@app.post("/v1/review_files", response_model=FilesReviewResponse, tags=["review"]) +async def review_files_endpoint(req: FilesReviewRequest) -> FilesReviewResponse: + total_chars = sum(len(f.content) for f in req.files) + if total_chars > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"文件内容总过长(限制 {settings.max_code_chars} 字符)", + ) + files = [ + {"filename": f.filename, "content": f.content, "language": f.language} + for f in req.files + ] + try: + result = review_files(files=files, context=req.context) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + return FilesReviewResponse( + model=settings.llm_model, + total_files=len(files), + file_reports=result["file_reports"], + overall_report=result["overall_report"], + ) + + @app.get("/v1/rules", tags=["review"]) def list_rules() -> dict: """List all built-in rule engine rules.""" @@ -111,6 +170,32 @@ def list_rules() -> dict: } +@app.get("/v1/rules/{rule_id}", tags=["review"]) +def get_rule(rule_id: str) -> dict: + """Explain a single rule-engine rule in detail.""" + result = explain_issue(rule_id) + if not result.get("ok"): + raise HTTPException(status_code=404, detail=result.get("error")) + return result + + +@app.post("/v1/suggest_fix", tags=["review"]) +async def suggest_fix(req: ReviewRequest) -> dict: + """Generate a full corrected version of code with known issues.""" + if len(req.code) > settings.max_code_chars: + raise HTTPException( + status_code=413, + detail=f"code 过长(限制 {settings.max_code_chars} 字符)", + ) + try: + result = suggest_fix_for_code( + code=req.code, language=req.language, context=req.context + ) + except ReviewError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + return {"ok": True, "model": settings.llm_model, "result": result} + + @app.get("/", response_class=HTMLResponse, include_in_schema=False) def index() -> HTMLResponse: html = Path(__file__).resolve().parent.parent / "web" / "index.html" @@ -127,6 +212,11 @@ async def http_exception_handler(request: Request, exc: HTTPException): ) +# Mount MCP catch-all AFTER all REST routes so /health, /v1/* etc. win. +if mcp_app is not None: + app.mount("/", mcp_app) + + def main() -> None: import uvicorn diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py index 91efae2..db650ec 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/mcp_server.py @@ -2,54 +2,100 @@ """FastMCP server exposing code review capabilities as reusable MCP tools. Tools: -- review_code: dual-engine review of a source code snippet -- review_diff: dual-engine review of a unified diff / PR change -- detect_security: fast rule-only security scan (no LLM, instant) -- list_rules: list all built-in rule engine rules +- review_code: dual-engine review of a source code snippet [LLM] +- review_diff: dual-engine review of a unified diff / PR [LLM] +- review_files: dual-engine review of multiple files [LLM] +- detect_security: fast rule-only security scan (instant, free) +- list_rules: list all built-in rule engine rules (instant) +- explain_issue: explain a rule in detail (instant, free) +- suggest_fix: generate corrected code for known issues [LLM] -Run via stdio (default) for Claude Code / Codex / Cursor. +Usage guidance for agents: +1. Start cheap: use detect_security / list_rules / explain_issue first + (no LLM cost, millisecond latency). +2. For a full analysis call review_code / review_diff / review_files with + detail="brief" (default) to save context tokens; use detail="full" when + the user needs every fix suggestion. +3. When the user wants the problem fixed, call suggest_fix with the code. + +Run via stdio (default) for Claude Code / Codex / Cursor, or mount +`mcp` onto a FastAPI app for remote streamable HTTP transport +(see app.main: `app.mount("/mcp", mcp.http_app())`). """ import json from fastmcp import FastMCP from .config import PROJECT_SLUG -from .reviewer import ReviewError, review_code, review_diff +from .reviewer import ( + ReviewError, + _build_brief_report, + explain_issue, + review_code, + review_diff, + review_files, + suggest_fix_for_code, +) from .rules_engine import RULES, run_rules mcp = FastMCP( PROJECT_SLUG, instructions=( - "Dual-engine code review assistant. Combines a rule-based static " - "analysis engine with LLM semantic review for cross-validated " - "quality reports. Tools: review_code, review_diff, " - "detect_security, list_rules." + "Dual-engine code review assistant. Rule engine + LLM semantic review " + "with cross-validation. Free instant tools (no LLM call): " + "detect_security, list_rules, explain_issue. LLM tools: review_code, " + "review_diff, review_files (use detail='brief' to save context unless " + "the user needs full details), suggest_fix. Workflow: quick scan with " + "detect_security first, then deep review, then explain_issue/suggest_fix " + "as needed." ), ) +def _wrap_review(report: dict, detail: str, extra: dict | None = None) -> str: + """Serialize a review report, applying brief/full detail control.""" + out = {"ok": True} + if extra: + out.update(extra) + if detail == "full": + report_with_meta = dict(report) + report_with_meta["detail"] = "full" + out["report"] = report_with_meta + else: + brief = _build_brief_report(report) + brief["detail"] = "brief" + out["report"] = brief + return json.dumps(out, ensure_ascii=False) + + @mcp.tool() def review_code_tool( code: str, language: str = "", context: str = "", + detail: str = "brief", ) -> str: - """Review source code with dual-engine (rules + LLM) and return a structured report. + """Review a source code snippet with dual-engine analysis (rules + LLM). + + Use this when you have a piece of code (not a diff) you want reviewed + for security, correctness, performance, maintainability issues. Args: code: source code to review. language: programming language hint (python, java, js, go, ...). context: optional description of what the code is supposed to do. + detail: "brief" (default, trimmed issues to save context) or "full" + (complete report with every description/suggestion/fix_code). Returns: - JSON string with summary, score, grade, issues (with source attribution), - strengths, improvements, and engine_info. + JSON string with summary, score, grade, dimension scores, issues + (source-attributed) and engine stats. """ try: report = review_code(code=code, language=language, context=context) except ReviewError as exc: return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) - return json.dumps({"ok": True, "report": report}, ensure_ascii=False) + return _wrap_review(report, detail) @mcp.tool() @@ -57,21 +103,106 @@ def review_diff_tool( diff: str, language: str = "", context: str = "", + detail: str = "brief", ) -> str: - """Review a unified diff (e.g. git diff output) for change-level risks. + """Review a unified diff (git diff / PR change) for change-level risks. + + Use this when the user shares a diff or asks you to check a PR / commit + change, not full source files. Args: diff: unified diff text. language: programming language hint. context: optional description of the change purpose. + detail: "brief" (default) or "full". Returns: - JSON string with diff metadata and a structured review report. + JSON string with diff metadata (files_changed, added/deleted lines) + and a structured review report. """ try: result = review_diff(diff=diff, language=language, context=context) except ReviewError as exc: return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + diff_meta = result.pop("diff_meta", {}) + return _wrap_review(result, detail, extra={"diff_meta": diff_meta}) + + +@mcp.tool() +def review_files_tool( + files: list[dict], + context: str = "", + detail: str = "brief", +) -> str: + """Review multiple files (entire module/project change) with dual-engine analysis. + + Use this when reviewing a whole set of files at once, e.g. before committing + or when several files changed together. Detects per-file rule hits plus + cross-file architecture problems. + + Args: + files: array of file objects, each {"filename": str, "content": str, + "language": str (optional)}. + context: optional project/task context description. + detail: "brief" (default) or "full". + + Returns: + JSON string with per-file reports and an overall cross-file report. + """ + try: + result = review_files(files=files, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + if detail == "full": + out = result + else: + overall = result.get("overall_report", {}) + out = { + "file_count": len(result.get("file_reports", [])), + "overall_report": _build_brief_report(overall, max_issues=8), + } + out["detail"] = detail + return json.dumps({"ok": True, "result": out}, ensure_ascii=False) + + +@mcp.tool() +def explain_issue(rule_id: str) -> str: + """Explain a rule-engine rule in detail (no LLM call, instant, free). + + Use this to understand why a rule fired or to explain a finding to the + user. Args: rule_id, e.g. PY-S001, JS-S002, AI-H003. + + Returns: + JSON string with rule definition, severity, category and guidance. + """ + result = explain_issue(rule_id) + return json.dumps(result, ensure_ascii=False) + + +@mcp.tool() +def suggest_fix( + code: str, + language: str = "", + context: str = "", +) -> str: + """Generate a full corrected version of code with known issues (LLM). + + Use this when the user wants the code actually fixed, not just reviewed. + Rules engine runs first to surface deterministic findings, then the LLM + produces a complete fixed_code block that can replace the original. + + Args: + code: source code to fix. + language: programming language hint (python, java, js, go, ...). + context: optional description of what the code is supposed to do. + + Returns: + JSON string with fixed_code, explanation, and list of changes. + """ + try: + result = suggest_fix_for_code(code=code, language=language, context=context) + except ReviewError as exc: + return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) return json.dumps({"ok": True, "result": result}, ensure_ascii=False) @@ -79,6 +210,9 @@ def review_diff_tool( def detect_security(code: str, language: str = "") -> str: """Fast rule-only security scan — no LLM call, returns instantly. + Use this for a quick cheap security check while coding; call review_code + for deeper semantic analysis. + Args: code: source code to scan. language: programming language hint. @@ -114,7 +248,7 @@ def detect_security(code: str, language: str = "") -> str: @mcp.tool() def list_rules() -> str: - """List all built-in rule engine rules with their metadata. + """List all built-in rule engine rules with their metadata (instant, free). Returns: JSON string with all rules (id, language, severity, category, title). @@ -143,4 +277,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py index f25ded6..2502431 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/prompts.py @@ -23,8 +23,15 @@ 评审报告 JSON 结构如下: { "summary": "一段 2-4 句的总体评价,指出最关键的问题与整体质量", - "score": 0到100的整数, + "score": 0到100的整数(综合评分), "grade": "由 score 派生,90-100 为 A,75-89 为 B,60-74 为 C,60 以下为 D", + "dimension_scores": { + "correctness": 0到100的整数,评估逻辑正确性、边界条件、错误处理, + "security": 0到100的整数,评估安全漏洞风险(注入、泄露、RCE等), + "performance": 0到100的整数,评估时间/空间复杂度、资源使用效率, + "maintainability": 0到100的整数,评估可读性、模块化、耦合度, + "best_practice": 0到100的整数,评估是否符合语言/框架最佳实践 + }, "issues": [ { "severity": "critical 或 major 或 minor 或 info", @@ -32,7 +39,8 @@ "line": 问题所在的大致行号(1-based),无法确定时填 null, "title": "一句话问题标题", "description": "问题详细描述,说明为什么是问题、可能后果", - "suggestion": "具体可行的修复建议,尽量给出示例代码" + "suggestion": "具体可行的修复建议说明", + "fix_code": "修复后的完整代码片段,可直接替换问题行;若无法生成具体代码则填 null" } ], "strengths": ["代码优点列表,至少 1 条"], @@ -42,8 +50,14 @@ 规则: 1. issues 中必须至少包含一条真正存在的问题;若确实没有问题,severity 用 info 说明代码状态优秀。 2. 不要编造代码中不存在的问题。security 类别优先于其他类别报告。 -3. 建议要具体、可执行,需要时可附简短示例代码。 +3. 建议要具体、可执行。fix_code 必须是可直接替换问题行的完整代码片段,包含修复后的代码,不要包含原始问题代码。 4. 若规则引擎预检结果中存在误报,请在 issues 中用 severity=info 说明"规则引擎 XX 为误报"。 +5. dimension_scores 中每个维度的评分必须与 issues 中对应类别的问题严重程度一致: + - 该维度无问题:85-100 + - 仅有 minor/info 级别问题:70-84 + - 有 major 级别问题:50-69 + - 有 critical 级别问题:0-49 +6. score 应为 dimension_scores 五个维度的加权平均(security 和 correctness 权重更高)。 """ DIFF_SYSTEM_PROMPT = """\ @@ -102,3 +116,93 @@ def build_diff_prompt(language: str, context: str, diff: str, diff_meta: str) -> parts.append("```diff\n" + diff + "\n```") parts.append(JSON_SCHEMA_EXAMPLE) return "\n".join(parts) + + +FILES_SYSTEM_PROMPT = """\ +你是一名资深软件架构师与代码评审专家,正在评审一个项目的多个文件。\ +你的任务是从全局视角评审代码质量:不仅关注单个文件内部的问题,\ +还要关注跨文件的架构问题(如循环依赖、接口不一致、重复逻辑等)。 + +请以严格的 JSON 格式输出评审报告,不要输出任何 JSON 以外的内容。\ +报告中的 issues 应涵盖所有文件的问题,line 字段使用问题所在文件内的行号,\ +在 title 中标注文件名前缀,如 "[utils.py] 第3行存在..."。 +""" + + +def build_files_prompt( + context: str, + files: list[dict], + rule_summary: str, +) -> str: + """Build prompt for multi-file review. + + Args: + context: optional project/task context. + files: list of {filename, language, content} dicts. + rule_summary: pre-check summary from rule engine. + """ + parts = [] + if context: + parts.append(f"项目上下文:{context}") + parts.append(f"共 {len(files)} 个文件待评审:\n") + + for f in files: + parts.append(f"--- 文件: {f['filename']} (语言: {f.get('language', '未知')}) ---") + parts.append(f"```{f.get('language', '')}\n{f['content']}\n```") + parts.append("") + + if rule_summary: + parts.append("规则引擎预检结果:") + parts.append(rule_summary) + else: + parts.append("规则引擎预检结果:未发现已知模式问题。") + + parts.append("") + parts.append(JSON_SCHEMA_EXAMPLE) + return "\n".join(parts) + + +SUGGEST_FIX_SYSTEM_PROMPT = """\ +你是一名资深代码修复专家。你的任务是为存在问题的代码生成修复方案。\ +请以严格的 JSON 格式输出,不要输出任何 JSON 以外的内容。 +""" + +SUGGEST_FIX_SCHEMA_EXAMPLE = """\ +修复方案 JSON 结构如下: +{ + "fixed_code": "修复后的完整代码,可直接替换原始代码;若无法修复则填 null", + "explanation": "2-4 句说明修改了什么、为什么这样修改、解决了什么问题", + "changes": ["修改点列表,每条描述一处具体修改"] +} + +规则: +1. fixed_code 必须完整、可直接运行,不含注释掉的原始代码。 +2. 若问题是缺失上下文导致无法准确修复,explanation 说明需要补充哪些信息,fixed_code 填 null。 +""" + + +def build_suggest_fix_prompt( + language: str, + context: str, + code: str, + issues: list[dict], +) -> str: + """Build prompt for generating a fix for known issues in code.""" + parts = [f"语言:{language or '未知'}"] + if context: + parts.append(f"任务上下文:{context}") + parts.append("存在问题:") + for i in issues: + parts.append( + f" - [{i.get('rule_id') or i.get('source', 'llm')}] " + f"{i.get('severity')}/{i.get('category')} " + f"行{i.get('line', '?')}: {i.get('title')}" + ) + if i.get("description"): + parts.append(f" 描述:{i['description']}") + parts.append("") + parts.append("原始代码:") + parts.append("```" + language + "\n" + code + "\n```") + parts.append("") + parts.append(SUGGEST_FIX_SCHEMA_EXAMPLE) + return "\n".join(parts) diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py index b156e6f..d9751a4 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/reviewer.py @@ -21,8 +21,12 @@ from .diff_parser import diff_summary, parse_diff from .prompts import ( DIFF_SYSTEM_PROMPT, + FILES_SYSTEM_PROMPT, + SUGGEST_FIX_SYSTEM_PROMPT, SYSTEM_PROMPT_WITH_RULES, build_diff_prompt, + build_files_prompt, + build_suggest_fix_prompt, build_user_prompt, build_user_prompt_with_rules, ) @@ -30,6 +34,62 @@ logger = logging.getLogger(__name__) +_SEVERITY_PENALTY = {"critical": 35, "major": 18, "minor": 8, "info": 3} +_DIMENSION_CATEGORIES = { + "correctness": ["correctness"], + "security": ["security"], + "performance": ["performance"], + "maintainability": ["maintainability"], + "best_practice": ["best_practice", "ai_pattern"], +} + + +def _compute_dimension_scores( + llm_scores: dict[str, int] | None, + merged_issues: list[dict], +) -> dict[str, int]: + """Compute final dimension scores by combining LLM scores with rule penalties. + + LLM provides a semantic baseline; rule-engine findings apply penalties + based on severity. This ensures deterministic rules always affect the + score even if the LLM misses them. + """ + defaults = { + "correctness": 85, + "security": 85, + "performance": 85, + "maintainability": 85, + "best_practice": 85, + } + if llm_scores: + for k in defaults: + if k in llm_scores: + defaults[k] = max(0, min(100, int(llm_scores[k]))) + + for issue in merged_issues: + cat = issue.get("category", "") + sev = issue.get("severity", "info") + penalty = _SEVERITY_PENALTY.get(sev, 3) + + for dim, cats in _DIMENSION_CATEGORIES.items(): + if cat in cats: + defaults[dim] = max(0, defaults[dim] - penalty) + + return defaults + + +def _compute_overall_score(dimensions: dict[str, int]) -> int: + """Weighted average: security & correctness weighted higher.""" + weights = { + "correctness": 0.25, + "security": 0.30, + "performance": 0.15, + "maintainability": 0.15, + "best_practice": 0.15, + } + total = sum(dimensions[k] * w for k, w in weights.items()) + return round(total) + class ReviewError(Exception): pass @@ -100,6 +160,7 @@ def review_code(code: str, language: str = "", context: str = "") -> dict[str, A "title": f.title, "description": f.description, "suggestion": f.suggestion, + "fix_code": f.fix_code, "confidence": f.confidence, } for f in rule_findings @@ -118,10 +179,22 @@ def review_code(code: str, language: str = "", context: str = "") -> dict[str, A llm_count = sum(1 for i in merged_issues if i.get("source") == "llm") confirmed_count = sum(1 for i in merged_issues if i.get("source") == "confirmed") + # ── Phase 4: Dimension scores ── + llm_dim_scores = llm_data.get("dimension_scores") + dimension_scores = _compute_dimension_scores(llm_dim_scores, merged_issues) + overall_score = _compute_overall_score(dimension_scores) + grade = ( + "A" if overall_score >= 90 + else "B" if overall_score >= 75 + else "C" if overall_score >= 60 + else "D" + ) + report = { "summary": llm_data.get("summary", ""), - "score": llm_data.get("score", 50), - "grade": llm_data.get("grade", "C"), + "score": overall_score, + "grade": grade, + "dimension_scores": dimension_scores, "issues": merged_issues, "strengths": llm_data.get("strengths", []), "improvements": llm_data.get("improvements", []), @@ -144,6 +217,13 @@ def review_diff(diff: str, language: str = "", context: str = "") -> dict[str, A "summary": "变更不包含实质性代码修改(仅删除或空白变更)。", "score": 100, "grade": "A", + "dimension_scores": { + "correctness": 100, + "security": 100, + "performance": 100, + "maintainability": 100, + "best_practice": 100, + }, "issues": [], "strengths": ["变更无引入新代码的风险"], "improvements": [], @@ -173,6 +253,7 @@ def review_diff(diff: str, language: str = "", context: str = "") -> dict[str, A "title": f.title, "description": f.description, "suggestion": f.suggestion, + "fix_code": f.fix_code, "confidence": f.confidence, } for f in rule_findings @@ -187,10 +268,21 @@ def review_diff(diff: str, language: str = "", context: str = "") -> dict[str, A llm_count = sum(1 for i in merged_issues if i.get("source") == "llm") confirmed_count = sum(1 for i in merged_issues if i.get("source") == "confirmed") + llm_dim_scores = llm_data.get("dimension_scores") + dimension_scores = _compute_dimension_scores(llm_dim_scores, merged_issues) + overall_score = _compute_overall_score(dimension_scores) + grade = ( + "A" if overall_score >= 90 + else "B" if overall_score >= 75 + else "C" if overall_score >= 60 + else "D" + ) + report = { "summary": llm_data.get("summary", ""), - "score": llm_data.get("score", 50), - "grade": llm_data.get("grade", "C"), + "score": overall_score, + "grade": grade, + "dimension_scores": dimension_scores, "issues": merged_issues, "strengths": llm_data.get("strengths", []), "improvements": llm_data.get("improvements", []), @@ -209,3 +301,211 @@ def review_diff(diff: str, language: str = "", context: str = "") -> dict[str, A }, } return report + + +def _build_report( + llm_data: dict[str, Any], + merged_issues: list[dict], + total_rules_run: int, +) -> dict[str, Any]: + """Build a standard review report from LLM data and merged issues.""" + rule_count = sum(1 for i in merged_issues if i.get("source") == "rule") + llm_count = sum(1 for i in merged_issues if i.get("source") == "llm") + confirmed_count = sum(1 for i in merged_issues if i.get("source") == "confirmed") + + llm_dim_scores = llm_data.get("dimension_scores") + dimension_scores = _compute_dimension_scores(llm_dim_scores, merged_issues) + overall_score = _compute_overall_score(dimension_scores) + grade = ( + "A" if overall_score >= 90 + else "B" if overall_score >= 75 + else "C" if overall_score >= 60 + else "D" + ) + + return { + "summary": llm_data.get("summary", ""), + "score": overall_score, + "grade": grade, + "dimension_scores": dimension_scores, + "issues": merged_issues, + "strengths": llm_data.get("strengths", []), + "improvements": llm_data.get("improvements", []), + "engine_info": { + "rule_count": rule_count, + "llm_count": llm_count, + "confirmed_count": confirmed_count, + "total_rules_run": total_rules_run, + "engines": ["rule", "llm"], + }, + } + + +def review_files( + files: list[dict[str, str]], + context: str = "", +) -> dict[str, Any]: + """Review multiple files: per-file rule scan + holistic LLM review. + + Args: + files: list of {filename, content, language} dicts. + context: optional project/task context. + + Returns: + dict with file_reports (per-file) and overall_report (holistic). + """ + all_rule_findings: list = [] + rule_summary_parts: list[str] = [] + file_reports: list[dict] = [] + + for f in files: + filename = f["filename"] + content = f["content"] + lang = f.get("language", "") + + file_findings = run_rules(content, lang) + all_rule_findings.extend(file_findings) + + if file_findings: + for finding in file_findings: + rule_summary_parts.append( + f" [{filename}] {finding.rule_id} {finding.severity}/" + f"{finding.category} 行{finding.line}: {finding.title}" + ) + + file_merged = merge_findings(file_findings, [], content) + file_report = _build_report( + {"summary": f"规则引擎扫描 {filename},发现 {len(file_findings)} 个问题。", + "strengths": [], "improvements": []}, + file_merged, + len(file_findings), + ) + file_reports.append({ + "filename": filename, + "language": lang, + "report": file_report, + }) + + rule_summary = "\n".join(rule_summary_parts) if rule_summary_parts else "" + + files_for_prompt = [ + {"filename": f["filename"], "language": f.get("language", ""), + "content": f["content"]} + for f in files + ] + user_prompt = build_files_prompt(context, files_for_prompt, rule_summary) + llm_data = _call_llm(FILES_SYSTEM_PROMPT, user_prompt) + llm_issues = llm_data.get("issues", []) + + all_code = "\n\n".join(f["content"] for f in files) + overall_merged = merge_findings(all_rule_findings, llm_issues, all_code) + overall_report = _build_report(llm_data, overall_merged, len(all_rule_findings)) + + return { + "file_reports": file_reports, + "overall_report": overall_report, + } + + +def suggest_fix_for_code( + code: str, + language: str = "", + context: str = "", +) -> dict[str, Any]: + """Generate a complete fix for code with known issues (LLM). + + Runs the rule engine first to surface deterministic findings, then asks + the LLM to produce a fully corrected version of the code. + """ + rule_findings = run_rules(code, language) + issues = [ + { + "rule_id": f.rule_id, + "severity": f.severity, + "category": f.category, + "line": f.line, + "title": f.title, + "description": f.description, + "suggestion": f.suggestion, + } + for f in rule_findings + ] + + user_prompt = build_suggest_fix_prompt(language, context, code, issues) + llm_data = _call_llm(SUGGEST_FIX_SYSTEM_PROMPT, user_prompt) + + result = { + "fixed_code": llm_data.get("fixed_code"), + "explanation": llm_data.get("explanation", ""), + "changes": llm_data.get("changes", []), + "found_issues": len(issues), + } + if not result["fixed_code"] and not issues: + result.update( + { + "fixed_code": code, + "explanation": "未检测到问题,代码保持原样。", + "changes": [], + } + ) + return result + + +def explain_issue(rule_id: str) -> dict[str, Any]: + """Explain a rule-engine rule in detail (no LLM needed). + + Returns the rule definition, applicability, and guidance. + """ + from .rules_engine import RULES + + for rule in RULES: + if rule.id.lower() == rule_id.strip().lower(): + return { + "rule_id": rule.id, + "language": rule.language, + "severity": rule.severity, + "category": rule.category, + "confidence": rule.confidence, + "title": rule.title, + "description": rule.description, + "suggestion": rule.suggestion, + "ok": True, + } + return {"ok": False, "error": f"未找到规则 {rule_id}", "rule_id": rule_id} + + +_BRIEF_ISSUE_KEYS = ("severity", "category", "line", "title", "source", "rule_id", "confidence") + + +def _build_brief_report( + report: dict[str, Any], + max_issues: int = 5, +) -> dict[str, Any]: + """Build a compact version of a review report for MCP budget usage. + + Keeps the headline (summary, scores, engine stats) plus a trimmed issue + list without verbose description/suggestion/fix_code fields, so agents + can decide whether to dig deeper without burning context tokens. + """ + issues = report.get("issues", []) + kept = issues[:max_issues] + brief = { + "summary": report.get("summary", ""), + "score": report.get("score", 0), + "grade": report.get("grade", "C"), + "dimension_scores": report.get("dimension_scores", {}), + "issue_count": len(issues), + "truncated": len(issues) > max_issues, + "issues": [ + {k: i.get(k) for k in _BRIEF_ISSUE_KEYS if k in i} + for i in kept + ], + "engine_info": report.get("engine_info", {}), + } + strengths = report.get("strengths", []) + improvements = report.get("improvements", []) + if strengths: + brief["strengths"] = strengths[:3] + if improvements: + brief["improvements"] = improvements[:3] + return brief diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py index 1bb7a25..56811fe 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/rules_engine.py @@ -42,6 +42,7 @@ class Finding: suggestion: str confidence: float source: str = "rule" + fix_code: str | None = None def _r(rule_id, lang, sev, cat, pat, conf, title, desc, sug): return Rule(rule_id, lang, sev, cat, re.compile(pat, re.MULTILINE), conf, title, desc, sug) @@ -259,6 +260,72 @@ def detect_language(code: str, hint: str = "") -> str: def _line_number(code: str, pos: int) -> int: return code.count("\n", 0, pos) + 1 + +def _extract_line(code: str, pos: int) -> str: + """Extract the full line containing the given position.""" + start = code.rfind("\n", 0, pos) + 1 + end = code.find("\n", pos) + if end == -1: + end = len(code) + return code[start:end] + + +_FIX_GENERATORS: dict[str, callable] = {} + + +def _fix_gen(rule_id: str): + """Decorator to register a fix code generator for a rule.""" + def decorator(fn): + _FIX_GENERATORS[rule_id] = fn + return fn + return decorator + + +@_fix_gen("PY-S001") +def _fix_eval(match_text: str, full_line: str) -> str: + return full_line.replace("eval(", "ast.literal_eval(") + + +@_fix_gen("PY-S002") +def _fix_exec(match_text: str, full_line: str) -> str: + indent = full_line[: len(full_line) - len(full_line.lstrip())] + return f"{indent}# 重构:避免使用 exec(),改为安全的实现方式" + + +@_fix_gen("PY-S004") +def _fix_hardcoded_secret(match_text: str, full_line: str) -> str: + indent = full_line[: len(full_line) - len(full_line.lstrip())] + var_match = re.match(r"\s*(\w+)\s*=\s*['\"]", full_line) + var_name = var_match.group(1).upper() if var_match else "SECRET" + return f"{indent}{var_match.group(1) if var_match else 'secret'} = os.environ['{var_name}']" + + +@_fix_gen("PY-S006") +def _fix_sql_injection(match_text: str, full_line: str) -> str: + indent = full_line[: len(full_line) - len(full_line.lstrip())] + return f"{indent}# 使用参数化查询:cursor.execute(sql, (param,))" + + +@_fix_gen("JS-S001") +def _fix_js_eval(match_text: str, full_line: str) -> str: + return full_line.replace("eval(", "JSON.parse(") + + +@_fix_gen("JS-S002") +def _fix_innerhtml(match_text: str, full_line: str) -> str: + return full_line.replace("innerHTML", "textContent") + + +@_fix_gen("PY-B001") +def _fix_bare_except(match_text: str, full_line: str) -> str: + return full_line.replace("except:", "except (ValueError, TypeError) as e:") + + +@_fix_gen("AI-H003") +def _fix_swallowed_catch(match_text: str, full_line: str) -> str: + indent = full_line[: len(full_line) - len(full_line.lstrip())] + return f"{indent}catch (e) {{ logger.error(e); throw e; }}" + def run_rules(code: str, language: str = "") -> list[Finding]: """Run all applicable rules against the code and return findings.""" detected = detect_language(code, language) @@ -268,6 +335,14 @@ def run_rules(code: str, language: str = "") -> list[Finding]: if rule.language != "*" and rule.language != detected: continue for match in rule.pattern.finditer(code): + fix_code = None + generator = _FIX_GENERATORS.get(rule.id) + if generator: + try: + full_line = _extract_line(code, match.start()) + fix_code = generator(match.group(0), full_line) + except Exception: + fix_code = None findings.append(Finding( rule_id=rule.id, severity=rule.severity, @@ -278,6 +353,7 @@ def run_rules(code: str, language: str = "") -> list[Finding]: suggestion=rule.suggestion, confidence=rule.confidence, source="rule", + fix_code=fix_code, )) return findings @@ -310,6 +386,7 @@ def merge_findings( "title": f.title, "description": f.description, "suggestion": f.suggestion, + "fix_code": f.fix_code, "source": "rule", "rule_id": f.rule_id, "confidence": f.confidence, @@ -334,6 +411,8 @@ def merge_findings( m["confidence"] = min(1.0, m.get("confidence", 0.5) + 0.3) if not m.get("description") and issue.get("description"): m["description"] = issue["description"] + if issue.get("fix_code"): + m["fix_code"] = issue["fix_code"] break else: merged.append({ @@ -343,6 +422,7 @@ def merge_findings( "title": issue.get("title", ""), "description": issue.get("description", ""), "suggestion": issue.get("suggestion", ""), + "fix_code": issue.get("fix_code"), "source": "llm", "rule_id": None, "confidence": 0.7, diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py index a8155ef..042aa86 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/app/schemas.py @@ -36,15 +36,33 @@ class ReviewIssue(BaseModel): title: str description: str suggestion: str + fix_code: str | None = Field( + default=None, + description="可直接应用的修复代码片段,null 表示无法自动生成", + ) source: IssueSource = Field(default="llm", description="Which engine found this issue") rule_id: str | None = Field(default=None, description="Rule ID if from rule engine") confidence: float = Field(default=0.7, ge=0.0, le=1.0, description="Confidence score") +class DimensionScores(BaseModel): + correctness: int = Field(..., ge=0, le=100, description="正确性维度评分") + security: int = Field(..., ge=0, le=100, description="安全性维度评分") + performance: int = Field(..., ge=0, le=100, description="性能维度评分") + maintainability: int = Field(..., ge=0, le=100, description="可维护性维度评分") + best_practice: int = Field(..., ge=0, le=100, description="最佳实践维度评分") + + class ReviewReport(BaseModel): summary: str = Field(..., description="One-paragraph overall summary") score: int = Field(..., ge=0, le=100) grade: str = Field(..., description="A/B/C/D derived from score") + dimension_scores: DimensionScores = Field( + default_factory=lambda: DimensionScores( + correctness=80, security=80, performance=80, maintainability=80, best_practice=80 + ), + description="五维度分项评分", + ) issues: list[ReviewIssue] strengths: list[str] improvements: list[str] @@ -70,6 +88,31 @@ class DiffReviewResponse(BaseModel): report: ReviewReport +class FileItem(BaseModel): + filename: str = Field(..., description="文件名,用于标识和展示") + content: str = Field(..., min_length=1, description="文件内容") + language: str = Field(default="", description="编程语言提示") + + +class FilesReviewRequest(BaseModel): + files: list[FileItem] = Field(..., min_length=1, max_length=20, description="待评审文件列表") + context: str = Field(default="", max_length=2000, description="可选的项目/任务上下文") + + +class FileReviewReport(BaseModel): + filename: str + language: str + report: ReviewReport + + +class FilesReviewResponse(BaseModel): + ok: bool = True + model: str + total_files: int + file_reports: list[FileReviewReport] + overall_report: ReviewReport + + class HealthResponse(BaseModel): status: str commit: str diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py new file mode 100644 index 0000000..20d76c9 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""一键评审 git 改动,无需粘贴代码。 + +用法: + python cli.py 评审工作区未提交改动 (git diff) + python cli.py --staged 评审已暂存改动 (git diff --cached) + python cli.py --commit HEAD~1 评审最近一次提交的改动 + python cli.py --commit HEAD~3 评审最近 3 次提交的改动 + python cli.py src/utils.py 评审单个文件 + python cli.py --remote 用远程 Vercel 部署而非本地 +""" +import argparse +import json +import os +import subprocess +import sys +import urllib.request + +REMOTE_URL = "https://code-review-agent-ashy-six.vercel.app" +LOCAL_URL = "http://127.0.0.1:8000" + +_LANG_MAP = { + ".py": "python", ".js": "javascript", ".ts": "javascript", + ".jsx": "javascript", ".tsx": "javascript", + ".java": "java", ".go": "go", ".rs": "rust", + ".rb": "ruby", ".php": "php", ".c": "c", ".cpp": "c", + ".sh": "shell", ".bash": "shell", +} + + +def detect_lang(filepath: str) -> str: + ext = os.path.splitext(filepath)[1].lower() + return _LANG_MAP.get(ext, "") + + +def run_git(*args) -> str: + result = subprocess.run( + ["git"] + list(args), capture_output=True, text=True, encoding="utf-8" + ) + if result.returncode != 0: + print(f"git 错误: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + return result.stdout + + +def get_diff(staged: bool, commit: str | None) -> str: + if commit: + return run_git("diff", commit) + if staged: + return run_git("diff", "--cached") + return run_git("diff") + + +def call_api(base_url: str, endpoint: str, payload: dict) -> dict: + url = base_url + endpoint + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, data=data, headers={"Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + return json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", errors="replace") + print(f"API 错误 {e.code}: {body}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"连接失败: {e}\n请先启动服务: uvicorn app.main:app", file=sys.stderr) + sys.exit(1) + + +SEVERITY_ICON = {"critical": "🔴", "major": "🟠", "minor": "🟡", "info": "🔵"} +SOURCE_ICON = {"rule": "⚡", "llm": "🧠", "confirmed": "✅"} + + +def print_report(data: dict, is_diff: bool): + if not data.get("ok"): + print(f"❌ {data.get('error', '未知错误')}") + return + + report = data.get("report", {}) + score = report.get("score", 0) + grade = report.get("grade", "?") + dims = report.get("dimension_scores", {}) + + print(f"\n{'='*60}") + print(f" 评分: {score}/100 ({grade}级)") + if dims: + dim_labels = { + "correctness": "正确性", "security": "安全性", + "performance": "性能", "maintainability": "可维护性", + "best_practice": "最佳实践", + } + dim_str = " ".join(f"{dim_labels[k]}:{v}" for k, v in dims.items() if k in dim_labels) + print(f" {dim_str}") + + if is_diff and "files_changed" in data: + files = data.get("files_changed", []) + print(f" 变更: {', '.join(files)} +{data.get('added_lines',0)} -{data.get('removed_lines',0)}") + + info = report.get("engine_info", {}) + if info: + print(f" 引擎: 规则{info.get('rule_count',0)} LLM{info.get('llm_count',0)} 确认{info.get('confirmed_count',0)}") + + print(f"{'='*60}\n") + + summary = report.get("summary", "") + if summary: + print(f"📋 {summary}\n") + + issues = report.get("issues", []) + if not issues: + print("✅ 未发现问题\n") + return + + print(f"发现 {len(issues)} 个问题:\n") + for i in issues: + sev = i.get("severity", "info") + src = i.get("source", "llm") + line = f"行{i['line']}" if i.get("line") else "?" + icon = SEVERITY_ICON.get(sev, "•") + sicon = SOURCE_ICON.get(src, "") + rule = f" [{i['rule_id']}]" if i.get("rule_id") else "" + print(f" {icon} {sicon} {i.get('title', '')} ({line}){rule}") + print(f" {i.get('description', '')[:120]}") + if i.get("fix_code"): + print(f" 🔧 修复: {i['fix_code'][:100]}") + print() + + strengths = report.get("strengths", []) + if strengths: + print("👍 优点:") + for s in strengths[:3]: + print(f" • {s}") + print() + + +def main(): + parser = argparse.ArgumentParser(description="一键代码评审") + parser.add_argument("--staged", action="store_true", help="评审已暂存改动") + parser.add_argument("--commit", metavar="REF", help="评审指定提交的改动 (如 HEAD~1)") + parser.add_argument("--remote", action="store_true", help="用远程 Vercel 部署") + parser.add_argument("--url", metavar="URL", help="自定义 API 地址") + parser.add_argument("file", nargs="?", help="评审单个文件") + args = parser.parse_args() + + base = args.url or (REMOTE_URL if args.remote else LOCAL_URL) + + if args.file: + if not os.path.exists(args.file): + print(f"文件不存在: {args.file}", file=sys.stderr) + sys.exit(1) + code = open(args.file, "r", encoding="utf-8").read() + lang = detect_lang(args.file) + print(f"评审文件: {args.file} ({lang or '未知'})") + data = call_api(base, "/v1/review", {"code": code, "language": lang}) + print_report(data, is_diff=False) + else: + diff = get_diff(args.staged, args.commit) + if not diff.strip(): + print("没有检测到改动。") + return + line_count = diff.count("\n") + print(f"评审 diff: {line_count} 行改动") + data = call_api(base, "/v1/review_diff", {"diff": diff}) + print_report(data, is_diff=True) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py index 695a7f6..ecbb9ec 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/tests/test_app.py @@ -119,6 +119,289 @@ def test_list_rules_endpoint(): assert any(r["id"] == "PY-S001" for r in body["rules"]) +def test_get_rule_detail_exists(): + resp = client.get("/v1/rules/PY-S001") + assert resp.status_code == 200 + body = resp.json() + assert body["rule_id"] == "PY-S001" + assert "description" in body + assert body["ok"] is True + + +def test_get_rule_detail_not_found(): + resp = client.get("/v1/rules/XX-X999") + assert resp.status_code == 404 + + +def test_explain_issue_provides_guidance(): + from app.reviewer import explain_issue + + result = explain_issue("py-s002") + assert result["ok"] is True + assert result["rule_id"] == "PY-S002" + + +def test_suggest_fix_endpoint_validates_length(monkeypatch): + settings = get_settings() + monkeypatch.setattr(settings, "max_code_chars", 10) + resp = client.post( + "/v1/suggest_fix", + json={"code": "x" * 100, "language": "python"}, + ) + assert resp.status_code == 413 + + +def test_suggest_fix_reports_llm_error(monkeypatch): + from app import main as main_module + from app.reviewer import ReviewError + + def fake_suggest_fix(*args, **kwargs): + raise ReviewError("LLM API Key 未配置") + + monkeypatch.setattr(main_module, "suggest_fix_for_code", fake_suggest_fix) + resp = client.post( + "/v1/suggest_fix", + json={"code": "eval(x)", "language": "python"}, + ) + assert resp.status_code == 502 + assert "LLM API Key 未配置" in resp.text + + +# ── Integrated review flows (mock LLM) ──────────────────────── + +def _mock_llm_data(monkeypatch, llm_payload: dict, issues=None): + """Patch reviewer._call_llm to return a canned payload.""" + from app import reviewer as reviewer_module + + def fake_call(system_prompt: str, user_prompt: str) -> dict: + data = dict(llm_payload) + if issues is not None: + data.setdefault("issues", issues) + return data + + monkeypatch.setattr(reviewer_module, "_call_llm", fake_call) + + +def test_review_code_full_flow(monkeypatch): + from app.reviewer import review_code + + _mock_llm_data( + monkeypatch, + { + "summary": "存在安全风险。", + "dimension_scores": {"correctness": 88, "security": 40, + "performance": 90, "maintainability": 80, + "best_practice": 75}, + "strengths": ["结构清晰"], + "improvements": ["避免使用 eval"], + }, + issues=[ + {"severity": "critical", "category": "security", "line": 1, + "title": "eval 风险", "description": "RCE", + "suggestion": "用 ast.literal_eval"} + ], + ) + report = review_code("result = eval(x)", "python", "测试") + assert report["grade"] in ("A", "B", "C", "D") + assert "security" in report["dimension_scores"] + assert report["dimension_scores"]["security"] < report["dimension_scores"]["correctness"] + assert any(i["source"] == "confirmed" for i in report["issues"]) + assert "security" in report["issues"][0]["category"] + + +def test_review_diff_full_flow(monkeypatch): + from app.reviewer import review_diff + + _mock_llm_data( + monkeypatch, + {"summary": "diff 存在安全问题", "dimension_scores": None, + "strengths": [], "improvements": []}, + issues=[ + {"severity": "critical", "category": "security", "line": 1, + "title": "新增 eval", "description": "RCE", "suggestion": "不要用"} + ], + ) + diff = """--- a/a.py ++++ b/a.py +@@ -1,3 +1,4 @@ + def f(): +- return 1 ++ return eval(data) ++ pass +""" + result = review_diff(diff, "python") + assert result["diff_meta"]["files_changed"] == ["a.py"] + assert result["score"] < 90 + assert any(i["source"] in ("confirmed", "rule") for i in result["issues"]) + + +def test_review_files_full_flow(monkeypatch): + from app.reviewer import review_files + + _mock_llm_data( + monkeypatch, + {"summary": "多文件整体评审", "dimension_scores": None, + "strengths": [], "improvements": []}, + issues=[ + {"severity": "major", "category": "security", "line": 1, + "title": "[utils.py] 硬编码密钥", "description": "泄露风险", + "suggestion": "用环境变量"} + ], + ) + files = [ + {"filename": "utils.py", "content": 'token = "sk-1234567890"', "language": "python"}, + {"filename": "main.py", "content": "import utils\nutils.main()", "language": "python"}, + ] + result = review_files(files, "项目") + assert "overall_report" in result + assert len(result["file_reports"]) == 2 + assert any(i["source"] in ("rule", "confirmed") for i in result["overall_report"]["issues"]) + + +def test_review_empty_diff_no_llm_call(monkeypatch): + from app.reviewer import review_diff + + _mock_llm_data( + monkeypatch, + {"summary": "should not be called", "issues": []}, + ) + result = review_diff("--- a/x\n+++ b/x\n@@ -1 +0,0 @@\n-print(1)", "python") + assert result["score"] == 100 + assert result["grade"] == "A" + + +def test_suggest_fix_full_flow(monkeypatch): + from app.reviewer import suggest_fix_for_code + + _mock_llm_data( + monkeypatch, + { + "fixed_code": "import os\napi_key = os.environ['API_KEY']", + "explanation": "改用环境变量存储密钥。", + "changes": ["删除硬编码", "引入 os.environ"], + }, + ) + result = suggest_fix_for_code('api_key = "sk-1234567890"', "python") + assert result["found_issues"] >= 1 + assert "os.environ" in result["fixed_code"] + assert result["changes"] + + +def test_suggest_fix_clean_code(monkeypatch): + from app.reviewer import suggest_fix_for_code + + _mock_llm_data( + monkeypatch, + {"fixed_code": None, "explanation": "empty", "changes": []}, + ) + result = suggest_fix_for_code('x = 1\nprint("hello")', "python") + assert result["found_issues"] == 0 + assert result["fixed_code"] == 'x = 1\nprint("hello")' + + +# ── Brief report (MCP context saver) ───────────────────────── + +def test_build_brief_report_truncates(): + from app.reviewer import _build_brief_report + + report = { + "summary": "s", "score": 70, "grade": "C", + "dimension_scores": {"security": 50}, + "engine_info": {"rule_count": 2}, + "strengths": ["a"], "improvements": ["b"], + "issues": [ + {"severity": "major", "category": "security", "line": i, "title": "t", + "description": "d" * 10, "suggestion": "s", "fix_code": "f", + "source": "rule", "rule_id": "PY-S001", "confidence": 0.9} + for i in range(8) + ], + } + brief = _build_brief_report(report, max_issues=5) + assert brief["issue_count"] == 8 + assert brief["truncated"] is True + assert len(brief["issues"]) == 5 + assert "description" not in brief["issues"][0] + assert "fix_code" not in brief["issues"][0] + assert brief["issues"][0]["rule_id"] == "PY-S001" + assert brief["score"] == 70 + assert brief["strengths"] == ["a"] + + +def test_build_brief_report_full_when_small(): + from app.reviewer import _build_brief_report + + report = { + "summary": "s", "score": 95, "grade": "A", + "dimension_scores": {}, "engine_info": {}, + "issues": [{"severity": "info", "category": "maintainability", + "line": 1, "title": "t", "source": "llm"}], + } + brief = _build_brief_report(report, max_issues=5) + assert brief["truncated"] is False + assert len(brief["issues"]) == 1 + + +def test_mcp_review_code_detail_brief(monkeypatch): + import json + from app.mcp_server import review_code_tool + + _mock_llm_data( + monkeypatch, + {"summary": "s", "dimension_scores": None, "strengths": [], "improvements": []}, + issues=[ + {"severity": "major", "category": "security", "line": i * 10 + 1, + "title": "t", "description": "d" * 20, "suggestion": "fix"} + for i in range(6) + ], + ) + raw = review_code_tool("x = 1\nprint(x)", "python", "") + data = json.loads(raw) + assert data["ok"] is True + assert data["report"]["detail"] == "brief" + assert data["report"]["truncated"] is True + assert len(data["report"]["issues"]) == 5 + + +def test_mcp_review_code_detail_full(monkeypatch): + import json + from app.mcp_server import review_code_tool + + _mock_llm_data( + monkeypatch, + {"summary": "s", "dimension_scores": None, "strengths": [], "improvements": []}, + issues=[ + {"severity": "major", "category": "security", "line": 1, + "title": "t", "description": "detailed", "suggestion": "fix", + "fix_code": "fixed"} + ], + ) + raw = review_code_tool("x = 1\nprint(x)", "python", "", detail="full") + data = json.loads(raw) + assert data["report"]["detail"] == "full" + assert len(data["report"]["issues"]) == 1 + assert "description" in data["report"]["issues"][0] + + +def test_mcp_review_files_structured_param(monkeypatch): + import json + from app.mcp_server import review_files_tool + + _mock_llm_data( + monkeypatch, + {"summary": "s", "dimension_scores": None, "strengths": [], "improvements": []}, + issues=[], + ) + files = [ + {"filename": "a.py", "content": 'x = "sk-1234567890"', "language": "python"}, + {"filename": "b.py", "content": "print(1)", "language": "python"}, + ] + raw = review_files_tool(files, "proj") + data = json.loads(raw) + assert data["ok"] is True + assert data["result"]["file_count"] == 2 + assert data["result"]["detail"] == "brief" + + # ── Diff parser ───────────────────────────────────────────────── def test_parse_simple_diff(): @@ -181,6 +464,47 @@ def test_review_diff_requires_body(): assert resp.status_code == 422 +def test_review_files_requires_body(): + resp = client.post("/v1/review_files", json={}) + assert resp.status_code == 422 + + +def test_review_files_rejects_empty_list(): + resp = client.post("/v1/review_files", json={"files": []}) + assert resp.status_code == 422 + + +def test_review_files_validates_total_length(monkeypatch): + settings = get_settings() + monkeypatch.setattr(settings, "max_code_chars", 50) + resp = client.post( + "/v1/review_files", + json={"files": [ + {"filename": "a.py", "content": "x" * 100, "language": "python"}, + {"filename": "b.py", "content": "y" * 100, "language": "python"}, + ]}, + ) + assert resp.status_code == 413 + + +def test_review_files_reports_llm_error(monkeypatch): + from app import main as main_module + from app.reviewer import ReviewError + + def fake_review_files(*args, **kwargs): + raise ReviewError("LLM API Key 未配置") + + monkeypatch.setattr(main_module, "review_files", fake_review_files) + resp = client.post( + "/v1/review_files", + json={"files": [ + {"filename": "a.py", "content": "print(1)", "language": "python"}, + ]}, + ) + assert resp.status_code == 502 + assert "LLM API Key 未配置" in resp.text + + # ── JSON extraction ───────────────────────────────────────────── def test_extract_json_fenced(): @@ -200,3 +524,103 @@ def test_extract_json_invalid(): with pytest.raises(ReviewError): _extract_json("完全没有JSON") + + +# ── Dimension scores ─────────────────────────────────────────── + +def test_dimension_scores_compute(): + from app.reviewer import _compute_dimension_scores, _compute_overall_score + + issues = [ + {"category": "security", "severity": "critical"}, + {"category": "performance", "severity": "minor"}, + ] + scores = _compute_dimension_scores(None, issues) + assert scores["security"] <= 50 + assert scores["performance"] < 85 + assert scores["correctness"] == 85 + overall = _compute_overall_score(scores) + assert 0 <= overall <= 100 + + +def test_dimension_scores_with_llm_baseline(): + from app.reviewer import _compute_dimension_scores + + llm_scores = { + "correctness": 90, + "security": 70, + "performance": 85, + "maintainability": 80, + "best_practice": 75, + } + issues = [{"category": "security", "severity": "major"}] + scores = _compute_dimension_scores(llm_scores, issues) + assert scores["security"] == 70 - 18 + assert scores["correctness"] == 90 + + +def test_dimension_scores_ai_pattern_affects_best_practice(): + from app.reviewer import _compute_dimension_scores + + issues = [{"category": "ai_pattern", "severity": "major"}] + scores = _compute_dimension_scores(None, issues) + assert scores["best_practice"] < 85 + assert scores["security"] == 85 + + +def test_overall_score_weighted(): + from app.reviewer import _compute_overall_score + + dims = { + "correctness": 100, + "security": 0, + "performance": 100, + "maintainability": 100, + "best_practice": 100, + } + score = _compute_overall_score(dims) + assert score == 70 + + +# ── Fix code generation ─────────────────────────────────────── + +def test_fix_code_eval(): + code = "result = eval(user_input)" + findings = run_rules(code, "python") + f = next(x for x in findings if x.rule_id == "PY-S001") + assert f.fix_code is not None + assert "ast.literal_eval" in f.fix_code + + +def test_fix_code_innerhtml(): + code = "document.getElementById('x').innerHTML = userInput" + findings = run_rules(code, "javascript") + f = next(x for x in findings if x.rule_id == "JS-S002") + assert f.fix_code is not None + assert "textContent" in f.fix_code + + +def test_fix_code_hardcoded_secret(): + code = 'api_key = "sk-1234567890abcdef"' + findings = run_rules(code, "python") + f = next(x for x in findings if x.rule_id == "PY-S004") + assert f.fix_code is not None + assert "os.environ" in f.fix_code + + +def test_fix_code_in_merge_findings(): + rule_findings = run_rules("eval('1+1')", "python") + merged = merge_findings(rule_findings, [], "eval('1+1')") + assert any(m.get("fix_code") for m in merged) + + +def test_fix_code_llm_override_on_confirm(): + rule_findings = run_rules("eval('1+1')", "python") + llm_issues = [ + {"severity": "critical", "category": "security", "line": 1, + "title": "eval", "description": "RCE", "suggestion": "don't use eval", + "fix_code": "import ast\nresult = ast.literal_eval(user_input)"} + ] + merged = merge_findings(rule_findings, llm_issues, "eval('1+1')") + confirmed = next(m for m in merged if m["source"] == "confirmed") + assert confirmed["fix_code"] == "import ast\nresult = ast.literal_eval(user_input)" diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html index 556b6e4..2cf726a 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html @@ -1,87 +1,281 @@ - + -Code Review Agent - 双引擎 AI 代码评审 +Code Review Agent — 双引擎 AI 代码评审 + + -
-

Code Review Agent

-

双引擎 AI 代码评审 · 规则引擎 + LLM 语义分析 · 交叉验证降低误报

-
-
-
-
-
代码评审
-
Diff 评审
+ + + + + +
+
+
+
双引擎交叉验证 · 53 测试全绿 · 生产就绪
+

让每一行代码
都经过 双引擎评审

+

规则引擎做确定性静态扫描,LLM 做语义级深度分析,交叉验证降低误报。输出五维度评分和可直接应用的修复代码。

+
+ + +
+
+
22
内置规则
+
5
维度评分
+
7
MCP 工具
+
4
语言覆盖
+
+
+
+ + +
+
+
+

为什么选择双引擎

+

单一规则引擎只能发现已知模式,单一 LLM 容易漏报。双引擎交叉验证兼顾确定性与深度。

+
+
+
+
+

规则引擎 — 确定性扫描

+

22 条跨语言规则覆盖安全漏洞、性能反模式、AI 幻觉模式。毫秒级返回,零 API 成本,离线可跑。

+
+
+
🧠
+

LLM 语义分析 — 深度理解

+

接收规则预检结果后做语义级评审:逻辑错误、架构缺陷、业务边界。确认或否定规则命中,消除误报。

+
+
+
+

交叉验证 — 三级溯源

+

每个 issue 标注来源:rule(仅规则)、llm(仅 LLM)、confirmed(双引擎一致)。置信度自动提升。

+
+
+
📊
+

五维度评分 — 量化质量

+

正确性、安全性、性能、可维护性、最佳实践各 0–100 分。综合分按安全 30% + 正确性 25% 加权。

+
+
+
🔧
+

修复代码 — 可直接应用

+

每个 issue 附带 fix_code 片段,复制即可替换。规则引擎为 8 类关键规则自动生成,LLM 覆盖复杂场景。

+
+
+
🔌
+

MCP + REST — 双协议

+

7 个 MCP 工具供 Agent 调用,REST API 供集成。远程 /mcp 端点与 /v1/* 共存于同一服务器。

+
-
- - +
+
+ + +
+
+
+

在线试用

+

粘贴代码或 diff,点击评审。也可用 CLI 一键评审 git 改动:python cli.py --commit HEAD~1

- - - -

+
+ + +
+
+
+
+
点击「开始评审」查看结果
+
+ -
- -
尚未评审。点击「开始评审」调用双引擎评审。
+ + + +
+
+
+

API 一览

+

REST API 与 MCP 工具共存于同一服务器,部署后即可使用。

+
+
+
POST
/v1/review
评审源代码,返回五维度评分 + 问题列表 + 修复代码
+
POST
/v1/review_diff
评审 Unified Diff,返回变更元数据 + 评审报告
+
POST
/v1/review_files
多文件批量评审,per-file 扫描 + 跨文件架构分析
+
POST
/v1/suggest_fix
为问题代码生成完整修复版本(fixed_code + 变更说明)
+
GET
/v1/rules
列出全部 22 条规则引擎规则
+
GET
/v1/rules/{rule_id}
查看单条规则详情与修复指引
+
GET
/mcp
远程 MCP streamable HTTP 端点(7 个工具)
+
GET
/health
健康检查,返回部署 commit
+
+
+
+ + +
+ + +
+ + - + \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json index 3364c2d..4548a24 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json @@ -3,7 +3,7 @@ "name": "Code Review Agent", "slug": "kestarsheng-code-review-agent", "sourceRepository": "https://github.com/kestarsheng/code-review-agent", - "reviewCommit": "d23b13b0eff960d777ca724ed09b41db0dd94194", + "reviewCommit": "13a336d2b8242bfff8fc0655022d71493a4e4a23", "apiBaseUrl": "https://code-review-agent-ashy-six.vercel.app/v1", "healthCheckUrl": "https://code-review-agent-ashy-six.vercel.app/health", "deploymentProofUrl": "https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json" diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md index 9ae28c1..69125c0 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- Review commit: `d23b13b0eff960d777ca724ed09b41db0dd94194` +- Review commit: `13a336d2b8242bfff8fc0655022d71493a4e4a23` - API base URL: `https://code-review-agent-ashy-six.vercel.app/v1` - Authentication: none @@ -15,7 +15,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"status":"ok","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} +{"status":"ok","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} ``` ## 2. Deployment proof @@ -27,7 +27,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"d23b13b0eff960d777ca724ed09b41db0dd94194"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} ``` ## 3. Capability call From 7704db5e3721bc2e5df18061653c664c1f8739dc Mon Sep 17 00:00:00 2001 From: kestarsheng Date: Fri, 11 Sep 2026 11:58:58 +0800 Subject: [PATCH 04/15] fix: replace dev-jargon hero badge with product-facing copy --- .../kestarsheng-code-review-agent/SUBMISSION.md | 6 +++--- .../kestarsheng-code-review-agent/source/web/index.html | 2 +- .../kestarsheng-code-review-agent/submission.json | 2 +- .../kestarsheng-code-review-agent/verification/README.md | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md index 48cae19..8ce04b9 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -17,7 +17,7 @@ ## Source and reproducibility - **Source repository:** https://github.com/kestarsheng/code-review-agent -- **Review commit:** `13a336d2b8242bfff8fc0655022d71493a4e4a23` +- **Review commit:** `93194b53c924bff802a10e1f389905c592b90919` - **Source submitted in this PR:** `source/` - **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` - **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` @@ -28,12 +28,12 @@ The API must expose: ```json // GET /health -{"status":"ok","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} +{"status":"ok","commit":"93194b53c924bff802a10e1f389905c592b90919"} ``` ```json // GET /.well-known/xagent-verification.json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"93194b53c924bff802a10e1f389905c592b90919"} ``` ## Verification diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html index 2cf726a..2631f9c 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html @@ -196,7 +196,7 @@
-
双引擎交叉验证 · 53 测试全绿 · 生产就绪
+
双引擎评审 · 开箱即用 · 多语言支持

让每一行代码
都经过 双引擎评审

规则引擎做确定性静态扫描,LLM 做语义级深度分析,交叉验证降低误报。输出五维度评分和可直接应用的修复代码。

diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json index 4548a24..49d6f1f 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json @@ -3,7 +3,7 @@ "name": "Code Review Agent", "slug": "kestarsheng-code-review-agent", "sourceRepository": "https://github.com/kestarsheng/code-review-agent", - "reviewCommit": "13a336d2b8242bfff8fc0655022d71493a4e4a23", + "reviewCommit": "93194b53c924bff802a10e1f389905c592b90919", "apiBaseUrl": "https://code-review-agent-ashy-six.vercel.app/v1", "healthCheckUrl": "https://code-review-agent-ashy-six.vercel.app/health", "deploymentProofUrl": "https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json" diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md index 69125c0..cddbcdc 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- Review commit: `13a336d2b8242bfff8fc0655022d71493a4e4a23` +- Review commit: `93194b53c924bff802a10e1f389905c592b90919` - API base URL: `https://code-review-agent-ashy-six.vercel.app/v1` - Authentication: none @@ -15,7 +15,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"status":"ok","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} +{"status":"ok","commit":"93194b53c924bff802a10e1f389905c592b90919"} ``` ## 2. Deployment proof @@ -27,7 +27,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"13a336d2b8242bfff8fc0655022d71493a4e4a23"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"93194b53c924bff802a10e1f389905c592b90919"} ``` ## 3. Capability call From b4d42ef74f460f973a3014b64f33aeaa269ce018 Mon Sep 17 00:00:00 2001 From: kestarsheng Date: Fri, 11 Sep 2026 12:11:09 +0800 Subject: [PATCH 05/15] feat: add CI/CD integration (exit codes, --format json, --sarif, GitHub Action) --- .../SUBMISSION.md | 8 +- .../source/.github/workflows/code-review.yml | 76 +++++++++++++++++++ .../source/README.md | 28 +++++++ .../source/cli.py | 72 ++++++++++++++++-- .../submission.json | 4 +- .../verification/README.md | 8 +- 6 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md index 8ce04b9..72a1d49 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -1,4 +1,4 @@ -# Code Review Agent +# Code Review Agent ## Capability @@ -17,7 +17,7 @@ ## Source and reproducibility - **Source repository:** https://github.com/kestarsheng/code-review-agent -- **Review commit:** `93194b53c924bff802a10e1f389905c592b90919` +- **Review commit:** `c25d63d7d7ce8c9b9a22687ec6e0321d3576124a` - **Source submitted in this PR:** `source/` - **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` - **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` @@ -28,12 +28,12 @@ The API must expose: ```json // GET /health -{"status":"ok","commit":"93194b53c924bff802a10e1f389905c592b90919"} +{"status":"ok","commit":"c25d63d7d7ce8c9b9a22687ec6e0321d3576124a"} ``` ```json // GET /.well-known/xagent-verification.json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"93194b53c924bff802a10e1f389905c592b90919"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"c25d63d7d7ce8c9b9a22687ec6e0321d3576124a"} ``` ## Verification diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml new file mode 100644 index 0000000..d89d7cc --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml @@ -0,0 +1,76 @@ +name: Code Review Agent + +on: + pull_request: + branches: [main, master] + +permissions: + contents: read + security-events: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get PR diff + run: | + git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > pr.diff + echo "DIFF_LINES=$(wc -l < pr.diff)" >> $GITHUB_ENV + + - name: Skip if empty + if: env.DIFF_LINES == '0' + run: echo "No changes to review"; exit 0 + + - name: Review with Code Review Agent + run: | + RESPONSE=$(curl -s -X POST \ + https://code-review-agent-ashy-six.vercel.app/v1/review_diff \ + -H "Content-Type: application/json" \ + -d "$(jq -Rs '{diff: .}' < pr.diff)") + + echo "$RESPONSE" > review_result.json + + SCORE=$(echo "$RESPONSE" | jq -r '.report.score // 0') + ISSUES=$(echo "$RESPONSE" | jq -r '.report.issues | length') + CRITICAL=$(echo "$RESPONSE" | jq -r '[.report.issues[] | select(.severity == "critical")] | length') + + echo "::notice::Score: $SCORE/100 | Issues: $ISSUES | Critical: $CRITICAL" + + if [ "$CRITICAL" -gt "0" ]; then + echo "::error::Found $CRITICAL critical issues. Review failed." + exit 1 + fi + + - name: Export SARIF + if: always() + run: | + python3 -c " + import json, sys + data = json.load(open('review_result.json')) + report = data.get('report', {}) + sev_map = {'critical': 'error', 'major': 'error', 'minor': 'warning', 'info': 'note'} + results = [] + for i in report.get('issues', []): + results.append({ + 'ruleId': i.get('rule_id', 'llm'), + 'level': sev_map.get(i.get('severity', 'info'), 'note'), + 'message': {'text': i.get('title', '') + ' — ' + i.get('description', '')}, + 'locations': [{'physicalLocation': { + 'artifactLocation': {'uri': i.get('file', 'diff')}, + 'region': {'startLine': i.get('line', 1)}, + }}], + }) + sarif = {'\$schema': 'https://json.schemastore.org/sarif-2.1.0.json', 'version': '2.1.0', 'runs': [{'tool': {'driver': {'name': 'Code Review Agent', 'version': '1.0'}}, 'results': results}]} + json.dump(sarif, open('results.sarif', 'w'), indent=2) + " + + - name: Upload SARIF to GitHub Code Scanning + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: code-review-agent \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md index 44d8080..f4b45e1 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md @@ -54,10 +54,38 @@ python cli.py --staged # 评审已暂存改动 (git diff --cached) python cli.py --commit HEAD~1 # 评审最近一次提交 python cli.py src/utils.py # 评审单个文件 python cli.py --remote # 用远程 Vercel 部署(无需启动本地服务) +python cli.py --format json # 输出 JSON(机器可读,用于管道/CI) +python cli.py --sarif out.sarif # 导出 SARIF(GitHub Code Scanning 格式) ``` +退出码:`0` 无严重问题 | `2` 存在 critical/major(可做 CI 门禁)| `1` 运行错误 + 自动读取 git diff → 调 API → 输出带严重度图标、维度评分、修复代码的结构化报告。 +## CI/CD 集成 + +### GitHub Actions(PR 自动评审) + +项目自带 `.github/workflows/code-review.yml`,PR 到 main 时自动触发: + +1. 获取 PR diff → 调用 Code Review Agent API +2. 有 critical issue 时 Action 失败(阻断 merge) +3. 导出 SARIF 上传到 GitHub Code Scanning(issue 直接标注在 PR diff 行上) + +### pre-commit hook + +```bash +# .git/hooks/pre-commit +python cli.py --staged --remote || exit 1 # 有 critical/major 则阻止提交 +``` + +### SARIF + GitHub Code Scanning + +```bash +python cli.py --sarif results.sarif --remote +# 然后在 GitHub Action 中用 github/codeql-action/upload-sarif@v3 上传 +``` + ## API 一览 | Method | Endpoint | 说明 | diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py index 20d76c9..f10a0f9 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py @@ -9,6 +9,13 @@ python cli.py --commit HEAD~3 评审最近 3 次提交的改动 python cli.py src/utils.py 评审单个文件 python cli.py --remote 用远程 Vercel 部署而非本地 + python cli.py --format json 输出 JSON(机器可读,用于管道/CI) + python cli.py --sarif out.sarif 导出 SARIF(GitHub Code Scanning 格式) + +退出码: + 0 无问题或仅有 info/minor + 2 存在 critical 或 major 级别问题(可用作 CI 门禁) + 1 运行错误(git 失败、API 连接失败等) """ import argparse import json @@ -136,12 +143,51 @@ def print_report(data: dict, is_diff: bool): print() +def to_sarif(data: dict) -> dict: + report = data.get("report", {}) + issues = report.get("issues", []) + sev_map = {"critical": "error", "major": "error", "minor": "warning", "info": "note"} + results = [] + for i in issues: + results.append({ + "ruleId": i.get("rule_id", "llm"), + "level": sev_map.get(i.get("severity", "info"), "note"), + "message": {"text": i.get("title", "") + " — " + i.get("description", "")}, + "locations": [{ + "physicalLocation": { + "artifactLocation": {"uri": i.get("file", "reviewed")}, + "region": {"startLine": i.get("line", 1)}, + } + }], + }) + return { + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [{ + "tool": {"driver": {"name": "Code Review Agent", "version": "1.0"}}, + "results": results, + }], + } + + +def get_exit_code(data: dict) -> int: + if not data.get("ok"): + return 1 + issues = data.get("report", {}).get("issues", []) + for i in issues: + if i.get("severity") in ("critical", "major"): + return 2 + return 0 + + def main(): parser = argparse.ArgumentParser(description="一键代码评审") parser.add_argument("--staged", action="store_true", help="评审已暂存改动") parser.add_argument("--commit", metavar="REF", help="评审指定提交的改动 (如 HEAD~1)") parser.add_argument("--remote", action="store_true", help="用远程 Vercel 部署") parser.add_argument("--url", metavar="URL", help="自定义 API 地址") + parser.add_argument("--format", choices=["table", "json"], default="table", help="输出格式") + parser.add_argument("--sarif", metavar="FILE", help="导出 SARIF 格式到文件") parser.add_argument("file", nargs="?", help="评审单个文件") args = parser.parse_args() @@ -153,18 +199,34 @@ def main(): sys.exit(1) code = open(args.file, "r", encoding="utf-8").read() lang = detect_lang(args.file) - print(f"评审文件: {args.file} ({lang or '未知'})") + if args.format == "table": + print(f"评审文件: {args.file} ({lang or '未知'})") data = call_api(base, "/v1/review", {"code": code, "language": lang}) - print_report(data, is_diff=False) + is_diff = False else: diff = get_diff(args.staged, args.commit) if not diff.strip(): print("没有检测到改动。") return - line_count = diff.count("\n") - print(f"评审 diff: {line_count} 行改动") + if args.format == "table": + line_count = diff.count("\n") + print(f"评审 diff: {line_count} 行改动") data = call_api(base, "/v1/review_diff", {"diff": diff}) - print_report(data, is_diff=True) + is_diff = True + + if args.sarif: + sarif = to_sarif(data) + with open(args.sarif, "w", encoding="utf-8") as f: + json.dump(sarif, f, ensure_ascii=False, indent=2) + if args.format == "table": + print(f"SARIF 已导出到 {args.sarif}") + + if args.format == "json": + print(json.dumps(data, ensure_ascii=False, indent=2)) + else: + print_report(data, is_diff=is_diff) + + sys.exit(get_exit_code(data)) if __name__ == "__main__": diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json index 49d6f1f..87d55d8 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/submission.json @@ -1,9 +1,9 @@ -{ +{ "schemaVersion": 1, "name": "Code Review Agent", "slug": "kestarsheng-code-review-agent", "sourceRepository": "https://github.com/kestarsheng/code-review-agent", - "reviewCommit": "93194b53c924bff802a10e1f389905c592b90919", + "reviewCommit": "c25d63d7d7ce8c9b9a22687ec6e0321d3576124a", "apiBaseUrl": "https://code-review-agent-ashy-six.vercel.app/v1", "healthCheckUrl": "https://code-review-agent-ashy-six.vercel.app/health", "deploymentProofUrl": "https://code-review-agent-ashy-six.vercel.app/.well-known/xagent-verification.json" diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md index cddbcdc..55cd9f3 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/verification/README.md @@ -1,8 +1,8 @@ -# Verification evidence +# Verification evidence ## Prerequisites -- Review commit: `93194b53c924bff802a10e1f389905c592b90919` +- Review commit: `c25d63d7d7ce8c9b9a22687ec6e0321d3576124a` - API base URL: `https://code-review-agent-ashy-six.vercel.app/v1` - Authentication: none @@ -15,7 +15,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"status":"ok","commit":"93194b53c924bff802a10e1f389905c592b90919"} +{"status":"ok","commit":"c25d63d7d7ce8c9b9a22687ec6e0321d3576124a"} ``` ## 2. Deployment proof @@ -27,7 +27,7 @@ curl --fail --silent --show-error https://code-review-agent-ashy-six.vercel.app/ Expected response: ```json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"93194b53c924bff802a10e1f389905c592b90919"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"c25d63d7d7ce8c9b9a22687ec6e0321d3576124a"} ``` ## 3. Capability call From 61d86e9f87c5e7c8374769c69afd114871b10d54 Mon Sep 17 00:00:00 2001 From: kestarsheng Date: Fri, 11 Sep 2026 13:29:00 +0800 Subject: [PATCH 06/15] feat: bilingual README (EN+ZH) + demo page i18n switcher (default EN) --- .../SUBMISSION.md | 6 +- .../source/.github/workflows/code-review.yml | 8 +- .../source/README.md | 5 +- .../source/README_EN.md | 288 ++++++++++++++++++ .../source/cli.py | 8 +- .../source/web/index.html | 265 ++++++++++------ .../submission.json | 2 +- .../verification/README.md | 6 +- 8 files changed, 486 insertions(+), 102 deletions(-) create mode 100644 submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_EN.md diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md index 72a1d49..ca3fdaf 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/SUBMISSION.md @@ -17,7 +17,7 @@ ## Source and reproducibility - **Source repository:** https://github.com/kestarsheng/code-review-agent -- **Review commit:** `c25d63d7d7ce8c9b9a22687ec6e0321d3576124a` +- **Review commit:** `acfc116e6b192f5a13b806153973ff1481905398` - **Source submitted in this PR:** `source/` - **Run tests:** `pip install -r requirements.txt && pytest tests/ -v` - **Run locally:** `pip install -r requirements.txt && uvicorn app.main:app --reload` @@ -28,12 +28,12 @@ The API must expose: ```json // GET /health -{"status":"ok","commit":"c25d63d7d7ce8c9b9a22687ec6e0321d3576124a"} +{"status":"ok","commit":"acfc116e6b192f5a13b806153973ff1481905398"} ``` ```json // GET /.well-known/xagent-verification.json -{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"c25d63d7d7ce8c9b9a22687ec6e0321d3576124a"} +{"schemaVersion":1,"slug":"kestarsheng-code-review-agent","commit":"acfc116e6b192f5a13b806153973ff1481905398"} ``` ## Verification diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml index d89d7cc..e06663b 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/.github/workflows/code-review.yml @@ -56,12 +56,12 @@ jobs: results = [] for i in report.get('issues', []): results.append({ - 'ruleId': i.get('rule_id', 'llm'), + 'ruleId': str(i.get('rule_id') or 'llm'), 'level': sev_map.get(i.get('severity', 'info'), 'note'), - 'message': {'text': i.get('title', '') + ' — ' + i.get('description', '')}, + 'message': {'text': str(i.get('title', '')) + ' — ' + str(i.get('description', ''))}, 'locations': [{'physicalLocation': { - 'artifactLocation': {'uri': i.get('file', 'diff')}, - 'region': {'startLine': i.get('line', 1)}, + 'artifactLocation': {'uri': str(i.get('file', 'diff'))}, + 'region': {'startLine': int(i.get('line', 1))}, }}], }) sarif = {'\$schema': 'https://json.schemastore.org/sarif-2.1.0.json', 'version': '2.1.0', 'runs': [{'tool': {'driver': {'name': 'Code Review Agent', 'version': '1.0'}}, 'results': results}]} diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md index f4b45e1..a005757 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README.md @@ -1,7 +1,10 @@ # Code Review Agent + **双引擎 AI 代码质量评审服务**(Code Review as a Service)。规则引擎 + LLM 语义分析 + 交叉验证,输出带分维度评分和可直接应用修复代码的结构化报告。提供 REST API 与 MCP 工具,可被 Claude Code / Codex / Cursor 等 Agent 直接调用。 +> [English](README_EN.md) | 中文 + > Submission for **X-Agent AI MCP Hackathon 2026 · Open Innovation Challenge**. > > 在线演示:https://code-review-agent-ashy-six.vercel.app @@ -279,7 +282,7 @@ curl https:///.well-known/xagent-verification.json python -m pytest tests/ -v ``` -48 个单元测试,覆盖规则引擎、Diff 解析、五维度评分、修复代码生成、多文件评审与完整双引擎流程。 +53 个单元测试,覆盖规则引擎、Diff 解析、五维度评分、修复代码生成、多文件评审与完整双引擎流程。 ## License diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_EN.md b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_EN.md new file mode 100644 index 0000000..3596d57 --- /dev/null +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/README_EN.md @@ -0,0 +1,288 @@ +# Code Review Agent + +**Dual-engine AI code quality review service** (Code Review as a Service). Rule engine + LLM semantic analysis + cross-validation, producing structured reports with per-dimension scores and directly applicable fix code. Provides REST API and MCP tools, callable by Claude Code / Codex / Cursor and other Agents. + +> 中文 | [English](README.md) + +> Submission for **X-Agent AI MCP Hackathon 2026 · Open Innovation Challenge**. +> +> Live demo: https://code-review-agent-ashy-six.vercel.app + +## Dual-Engine Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Input: Code / Diff / Multi-file │ +└───────────────┬─────────────────────────────────────────┘ + ▼ +┌──────────────────────────┐ ┌─────────────────────────────┐ +│ ① Rule Engine (deterministic) │ │ ② LLM Semantic Analysis (deep) │ +│ · 22 cross-language rules │ │ · Receives rule pre-scan results │ +│ · Python/JS/Java/Go/C* │───▶ · Confirms/denies rule hits (removes false positives) │ +│ · Security/Perf/AI hallucination/style │ │ · Finds semantic issues (logic/architecture) │ +│ · Zero-cost, ms-level, offline │ │ · Generates per-dimension scores & fix_code │ +└───────────────┬──────────┘ └──────────────┬──────────────┘ + ▼ ▼ +┌───────────────────────────────────────────────────────────┐ +│ ③ Cross-Validation Merge (merge_findings) │ +│ · rule — rule engine only (high confidence retained) │ +│ · llm — LLM only │ +│ · confirmed — both engines agree (confidence +0.3, max 1.0) │ +└───────────────────────────────┬───────────────────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ ④ Output: 5-dimension scores + applicable fixes + traceability │ +│ · correctness/security/performance/maintainability/best_practice │ +│ · score = weighted avg (security 30% · correctness 25%) │ +│ · each issue includes fix_code (copy-paste ready) │ +└───────────────────────────────────────────────────────────┘ +``` + +## Features + +- **Dual-engine review** — Rule engine performs deterministic static scan first, LLM reviews with rule context, cross-validation reduces false positives +- **5-dimension scoring** — Correctness / Security / Performance / Maintainability / Best Practice, each 0–100, weighted composite score +- **Directly applicable fix code** — Rule engine auto-generates `fix_code` for 8 key rule types, LLM covers complex scenarios +- **Three review modes** — Single file code, Unified Diff (PR changes), Multi-file batch (cross-file architecture issues) +- **CLI one-click review** — `python cli.py` reads git diff directly, no pasting needed +- **MCP toolset** — 7 tools: review / diff review / multi-file review / security scan / rule explanation / fix generation / rule listing +- **Interactive demo page** — Dark mode, syntax highlighting, dimension score bars, engine visualization, "one-click apply fix" + +## CLI One-Click Review (Recommended) + +```bash +python cli.py # Review uncommitted changes (git diff) +python cli.py --staged # Review staged changes (git diff --cached) +python cli.py --commit HEAD~1 # Review the last commit +python cli.py src/utils.py # Review a single file +python cli.py --remote # Use remote Vercel deployment (no local server needed) +python cli.py --format json # Output JSON (machine-readable, for pipes/CI) +python cli.py --sarif out.sarif # Export SARIF (GitHub Code Scanning format) +``` + +Exit codes: `0` no serious issues | `2` critical/major found (CI gate) | `1` runtime error + +Auto-reads git diff → calls API → outputs structured report with severity icons, dimension scores, and fix code. + +## CI/CD Integration + +### GitHub Actions (PR Auto-Review) + +Includes `.github/workflows/code-review.yml`, auto-triggers on PR to main: + +1. Gets PR diff → calls Code Review Agent API +2. Fails Action if critical issues found (blocks merge) +3. Exports SARIF and uploads to GitHub Code Scanning (issues annotated on PR diff lines) + +### pre-commit hook + +```bash +# .git/hooks/pre-commit +python cli.py --staged --remote || exit 1 # Blocks commit if critical/major found +``` + +### SARIF + GitHub Code Scanning + +```bash +python cli.py --sarif results.sarif --remote +# Then upload in GitHub Action with github/codeql-action/upload-sarif@v3 +``` + +## API Overview + +| Method | Endpoint | Description | +| --- | --- | --- | +| `POST` | `/v1/review` | Review source code, return structured report | +| `POST` | `/v1/review_diff` | Review Unified Diff (PR changes) | +| `POST` | `/v1/review_files` | Multi-file batch review (cross-file architecture analysis) | +| `POST` | `/v1/suggest_fix` | Generate complete fixed version for problematic code | +| `GET` | `/v1/rules` | List all rule engine rules | +| `GET` | `/v1/rules/{rule_id}` | View single rule details and fix guidance | +| `GET` | `/health` | Health check, returns deployment commit | +| `GET` | `/.well-known/xagent-verification.json` | Deployment proof (slug + commit) | +| `GET` | `/` | Interactive demo page | + +## Quick start (local) + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env # Fill in LLM_API_KEY +uvicorn app.main:app --reload +``` + +Open http://127.0.0.1:8000 for the demo page, or http://127.0.0.1:8000/docs for Swagger. + +### Example: Review Code + +```bash +curl -X POST http://127.0.0.1:8000/v1/review \ + -H "Content-Type: application/json" \ + -d '{"code": "result = eval(user_input)", "language": "python"}' +``` + +Response (abridged): + +```json +{ + "report": { + "score": 68, + "grade": "C", + "dimension_scores": { + "correctness": 88, "security": 35, + "performance": 90, "maintainability": 80, "best_practice": 75 + }, + "issues": [ + { + "severity": "critical", + "category": "security", + "line": 1, + "title": "Using eval() to execute arbitrary code", + "description": "eval() executes arbitrary strings as code, posing a severe injection risk.", + "suggestion": "Use ast.literal_eval() or a dedicated parser.", + "fix_code": "result = ast.literal_eval(user_input)", + "source": "confirmed", + "rule_id": "PY-S001", + "confidence": 1.0 + } + ], + "engine_info": { + "rule_count": 0, "llm_count": 0, "confirmed_count": 1, + "total_rules_run": 3, "engines": ["rule", "llm"] + } + } +} +``` + +### Example: Review a Diff + +```bash +curl -X POST http://127.0.0.1:8000/v1/review_diff \ + -H "Content-Type: application/json" \ + -d '{"diff": "--- a/x.py\n+++ b/x.py\n@@ -1,3 +1,4 @@\n def f():\n- return 1\n+ return eval(data)", "language": "python"}' +``` + +Response includes `files_changed` / `added_lines` / `removed_lines` change metadata with the full report. + +### Example: Multi-file Review + +```json +{ + "context": "User service module", + "files": [ + {"filename": "utils.py", "content": "import os\napi_key = os.environ['KEY']", "language": "python"}, + {"filename": "main.py", "content": "from utils import *\nresult = eval(req.body)", "language": "python"} + ] +} +``` + +Returns per-file `file_reports` (rule scan) and one `overall_report` (LLM cross-file architecture review). + +## MCP Usage + +### Local stdio (Claude Code / Codex / Cursor) + +```bash +python -m app.mcp_server # stdio transport +``` + +Register in client config: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "python", + "args": ["-m", "app.mcp_server"] + } + } +} +``` + +### Remote streamable HTTP (same deployment, no local Python needed) + +After deployment, access `https:///mcp`, configure in MCP client: + +```json +{ + "mcpServers": { + "code-review-agent": { + "command": "npx", + "args": ["-y", "@anthropic-ai/mcp-client", "https:///mcp"] + } + } +} +``` + +> The remote MCP endpoint and REST API share the same server. After deployment, `/mcp` provides streamable HTTP protocol, `/v1/*` provides REST. + +### Usage Guide (for Agents) + +1. **Free quick scan first**: Use `detect_security` / `list_rules` / `explain_issue` (no LLM call, ms-level response) +2. **Deep review**: Use `review_code` / `review_diff` / `review_files`, default `detail="brief"` (saves context, returns title-level issues only) +3. **Full report when needed**: `detail="full"` returns complete description / suggestion / fix_code for each issue +4. **Fix**: Use `suggest_fix` to get directly replaceable `fixed_code` + +### MCP Tools + +| Tool | Parameters | LLM | Description | +| --- | --- | --- | --- | +| `review_code` | `code, language?, context?, detail?` | ✅ | Review source code (`detail: "brief"\|"full"`) | +| `review_diff` | `diff, language?, context?, detail?` | ✅ | Review Unified Diff | +| `review_files` | `files: [{filename, content, language?}], context?, detail?` | ✅ | Multi-file batch review (structured params, not JSON string) | +| `detect_security` | `code, language?` | ❌ | Rule engine security scan only, instant response | +| `explain_issue` | `rule_id` | ❌ | Explain a rule (definition/severity/fix guidance) | +| `suggest_fix` | `code, language?, context?` | ✅ | Return fixed code (fixed_code + change explanation) | +| `list_rules` | — | ❌ | List all rules | + +> `review_files` `files` parameter is a **structured array**, each element `{filename, content, language?}`. Agents don't need to manually compose JSON strings. + +## Rule Engine + +Built-in **22 cross-language rules** covering Python / JavaScript / Java / Go / C* / cross-language general patterns: + +| Category | Examples | +| --- | --- | +| Security | `eval`/`exec`, SQL injection, command injection, hardcoded secrets, `pickle.loads`, `innerHTML` XSS | +| Performance | Nested loops O(n²), dict iteration without `.items()`, pre-generating large lists | +| AI Pattern | Hallucinated imports of framework internals, `forEach` with `await`, catch swallowing exceptions | +| Maintainability / Best Practice | TODO/FIXME, bare `except`, missing type annotations | + +8 key rule types have **auto fix code generation** (`eval`→`ast.literal_eval`, `innerHTML`→`textContent`, hardcoded secret→`os.environ`, etc.). + +## Configuration (Environment Variables) + +| Var | Default | Description | +| --- | --- | --- | +| `LLM_BASE_URL` | `https://api.deepseek.com/v1` | OpenAI-compatible base URL | +| `LLM_API_KEY` | — | API key (required) | +| `LLM_MODEL` | `deepseek-chat` | Model name | +| `LLM_TIMEOUT_SECONDS` | `120` | LLM request timeout | +| `MAX_CODE_CHARS` | `60000` | Max characters per review | +| `COMMIT` | `dev` | Deployment commit, returned by /health and verification file | + +## Deployment + +- **Vercel** (current): `vercel.json` configured for Serverless service; push after setting env vars in Vercel project +- **Docker**: `docker build -t code-review-agent . && docker run -p 8000:8000 code-review-agent` +- **Render**: Use `render.yaml`, push repo and set env vars + +Post-deploy verification: + +```bash +curl https:///health +curl https:///.well-known/xagent-verification.json +``` + +## Testing + +```bash +python -m pytest tests/ -v +``` + +53 unit tests covering rule engine, diff parsing, 5-dimension scoring, fix code generation, multi-file review, and full dual-engine flow. + +## License + +UNLICENSED — submission-only use for X-Agent AI MCP Hackathon 2026. \ No newline at end of file diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py index f10a0f9..cf86109 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/cli.py @@ -150,13 +150,13 @@ def to_sarif(data: dict) -> dict: results = [] for i in issues: results.append({ - "ruleId": i.get("rule_id", "llm"), + "ruleId": str(i.get("rule_id") or "llm"), "level": sev_map.get(i.get("severity", "info"), "note"), - "message": {"text": i.get("title", "") + " — " + i.get("description", "")}, + "message": {"text": str(i.get("title", "")) + " — " + str(i.get("description", ""))}, "locations": [{ "physicalLocation": { - "artifactLocation": {"uri": i.get("file", "reviewed")}, - "region": {"startLine": i.get("line", 1)}, + "artifactLocation": {"uri": str(i.get("file", "reviewed"))}, + "region": {"startLine": int(i.get("line", 1))}, } }], }) diff --git a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html index 2631f9c..9cbb3d5 100644 --- a/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html +++ b/submissions/mcp-hackathon/kestarsheng-code-review-agent/source/web/index.html @@ -1,10 +1,10 @@ - + -Code Review Agent — 双引擎 AI 代码评审 - +Code Review Agent — Dual-Engine AI Code Review + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ workbench +

Live Workbench — free & instant

+

Deterministic engines run server-side — no LLM, no API key, millisecond latency. Try metrics, SARIF export, rule catalog, or PR URL review.

+
+
+
+
Metrics
+
SARIF Export
+
Rule Catalog
+
PR URL
+
+
+
+
+ + +
+ + +
+ + +
+
+
+
+
Click a tab above and run — results appear here instantly.
+
+
+
+
-

API Overview

-

REST API and MCP tools coexist on the same server, ready to use after deployment.

+ api +

12 endpoints. 9 MCP tools.

+

REST API and MCP tools coexist on the same server. SARIF export for GitHub Code Scanning. PR URL review for pull requests.

-
-
POST
/v1/review
Review source code, return 5-dimension scores + issues + fix code
-
POST
/v1/review_diff
Review Unified Diff, return change metadata + review report
-
POST
/v1/review_files
Multi-file batch review, per-file scan + cross-file analysis
-
POST
/v1/suggest_fix
Generate complete fixed version (fixed_code + change notes)
-
GET
/v1/rules
List all 26 rule engine rules
-
GET
/v1/rules/{rule_id}
View single rule details and fix guidance
-
GET
/mcp
Remote MCP streamable HTTP endpoint (7 tools)
-
GET
/health
Health check, returns deployment commit
+
+
POST/v1/reviewReview source code, return 5-dimension scores + issues + fix code
+
POST/v1/review_diffReview Unified Diff, return change metadata + review report
+
POST/v1/review_filesMulti-file batch review, per-file scan + cross-file analysis
+
POST/v1/review_prReview GitHub PR or commit URL by fetching diff remotely
+
POST/v1/suggest_fixGenerate complete fixed version (fixed_code + change notes)
+
POST/v1/metricsCode quality metrics: complexity, function lengths, comment ratio
+
POST/v1/sarifExport SARIF 2.1.0 for GitHub Code Scanning / VS Code
+
GET/v1/rulesList all 40 rule engine rules
+
GET/v1/rules/{rule_id}View single rule details and fix guidance
+
GET/mcpRemote MCP streamable HTTP endpoint (9 tools)
+
GET/healthHealth check, returns deployment commit
+
GET/verificationMCP tool verification report for hackathon judges
@@ -344,7 +539,7 @@

API Overview