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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ coverage/
# Misc
*.tmp
*.bak
*.orig
*.orig

# Grade reports
.grade-reports/
31 changes: 21 additions & 10 deletions crates/llm-router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,15 @@ impl OpenAiProvider {
impl LlmProvider for OpenAiProvider {
async fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse, LlmError> {
let start = std::time::Instant::now();

let body = serde_json::json!({
"model": request.model,
"messages": request.messages,
"temperature": request.temperature.unwrap_or(0.7),
});

let _response = self.client
let _response = self
.client
.post(format!("{}/chat/completions", self.base_url))
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&body)
Expand Down Expand Up @@ -141,19 +142,25 @@ impl LlmRouter {
self.fallback = Some(provider);
}

pub async fn complete(&self, request: &CompletionRequest) -> Result<CompletionResponse, LlmError> {
pub async fn complete(
&self,
request: &CompletionRequest,
) -> Result<CompletionResponse, LlmError> {
// Route based on model prefix
let (prefix, _) = request.model.split_once('/').unwrap_or((&request.model, ""));

let (prefix, _) = request
.model
.split_once('/')
.unwrap_or((&request.model, ""));

if let Some(provider) = self.providers.get(prefix) {
return provider.complete(request).await;
}

// Try fallback
if let Some(fallback) = &self.fallback {
return fallback.complete(request).await;
}

Err(LlmError::InvalidModel(request.model.clone()))
}
}
Expand Down Expand Up @@ -261,8 +268,12 @@ mod tests {
format!("{}", d),
];
// All four messages must be unique.
let unique: std::collections::HashSet<&str> =
messages.iter().map(|s| s.as_str()).collect();
assert_eq!(unique.len(), 4, "duplicate Display for variant: {:?}", messages);
let unique: std::collections::HashSet<&str> = messages.iter().map(|s| s.as_str()).collect();
assert_eq!(
unique.len(),
4,
"duplicate Display for variant: {:?}",
messages
);
}
}
52 changes: 38 additions & 14 deletions crates/mcp-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use thiserror::Error;
use tokio::sync::RwLock;

#[derive(Error, Debug)]
pub enum McpError {
Expand Down Expand Up @@ -69,27 +69,49 @@ impl McpServer {
}
}

pub async fn register_tool(&self, tool: Tool, handler: impl Fn(serde_json::Value) -> Result<serde_json::Value> + Send + Sync + 'static) {
self.tools.write().await.insert(tool.name.clone(), ToolHandler {
tool,
handler: Arc::new(handler),
});
pub async fn register_tool(
&self,
tool: Tool,
handler: impl Fn(serde_json::Value) -> Result<serde_json::Value> + Send + Sync + 'static,
) {
self.tools.write().await.insert(
tool.name.clone(),
ToolHandler {
tool,
handler: Arc::new(handler),
},
);
}

pub async fn register_resource(&self, resource: Resource) {
self.resources.write().await.insert(resource.uri.clone(), resource);
self.resources
.write()
.await
.insert(resource.uri.clone(), resource);
}

pub async fn list_tools(&self) -> Vec<Tool> {
self.tools.read().await.values().map(|h| h.tool.clone()).collect()
self.tools
.read()
.await
.values()
.map(|h| h.tool.clone())
.collect()
}

pub async fn call_tool(&self, name: &str, arguments: serde_json::Value) -> Result<ToolResult, McpError> {
pub async fn call_tool(
&self,
name: &str,
arguments: serde_json::Value,
) -> Result<ToolResult, McpError> {
let tools = self.tools.read().await;
let handler = tools.get(name).ok_or(McpError::ToolNotFound(name.to_string()))?;

let result = (handler.handler)(arguments).map_err(|e| McpError::InvalidRequest(e.to_string()))?;

let handler = tools
.get(name)
.ok_or(McpError::ToolNotFound(name.to_string()))?;
Comment on lines +108 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replace eager error construction with lazy error construction here to avoid clippy's or_fun_call warning under -D warnings. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The code eagerly constructs the error value inside ok_or(...), which means name.to_string() is evaluated even when the option is Some. This is the exact pattern that can trigger clippy's or_fun_call warning, so the suggestion correctly identifies a real issue.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/mcp-server/src/lib.rs
**Line:** 108:110
**Comment:**
	*Custom Rule: Replace eager error construction with lazy error construction here to avoid clippy's `or_fun_call` warning under `-D warnings`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


let result =
(handler.handler)(arguments).map_err(|e| McpError::InvalidRequest(e.to_string()))?;

Ok(ToolResult {
content: vec![ContentItem {
content_type: "text".to_string(),
Expand All @@ -105,7 +127,9 @@ impl McpServer {

pub async fn read_resource(&self, uri: &str) -> Result<String, McpError> {
let resources = self.resources.read().await;
let resource = resources.get(uri).ok_or(McpError::ResourceNotFound(uri.to_string()))?;
let resource = resources
.get(uri)
.ok_or(McpError::ResourceNotFound(uri.to_string()))?;
Comment on lines +130 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Use deferred error creation at this lookup site as well so clippy does not flag eager allocation when warnings are denied. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is the same eager error-construction pattern as the tool lookup above. The uri.to_string() allocation happens unconditionally inside ok_or(...), so the lint concern is real and the suggestion is verified.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/mcp-server/src/lib.rs
**Line:** 130:132
**Comment:**
	*Custom Rule: Use deferred error creation at this lookup site as well so clippy does not flag eager allocation when warnings are denied.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Ok(format!("Resource: {}", resource.name))
}
}
Expand Down
15 changes: 11 additions & 4 deletions crates/pheno-embedding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,22 @@ impl OpenAiEmbeddings {
}
}

pub async fn embed(&self, request: &EmbeddingRequest) -> Result<EmbeddingResponse, EmbeddingError> {
let model = request.model.clone().unwrap_or_else(|| "text-embedding-3-small".to_string());

pub async fn embed(
&self,
request: &EmbeddingRequest,
) -> Result<EmbeddingResponse, EmbeddingError> {
let model = request
.model
.clone()
.unwrap_or_else(|| "text-embedding-3-small".to_string());

let body = serde_json::json!({
"input": request.texts,
"model": model,
});

let _response = self.client
let _response = self

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Do not suppress the provider response with an underscore binding; consume and validate the HTTP response (status/body) and map it into the typed return value instead of silently discarding it. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The code sends the request and stores the result in _response, but never inspects the HTTP status or body before continuing. That is a real omission in the embedding path and matches the suggestion's complaint about discarding the provider response.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/pheno-embedding/src/lib.rs
**Line:** 65:72
**Comment:**
	*Custom Rule: Do not suppress the provider response with an underscore binding; consume and validate the HTTP response (status/body) and map it into the typed return value instead of silently discarding it.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

.client
.post("https://api.openai.com/v1/embeddings")
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&body)
Expand Down
198 changes: 198 additions & 0 deletions grade.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# grade.sh — Fleet-wide project grading engine
# Usage: ./grade.sh [--fast] [--json] [--html]
# --fast : Quick mode (skips heavy checks like fuzz, mutation, perf)
# --json : Output machine-readable JSON
# --html : Output HTML report

set -euo pipefail

FAST=false
JSON=false
HTML=false
REPORT_DIR=".grade-reports"

while [[ $# -gt 0 ]]; do
case "$1" in
--fast) FAST=true; shift ;;
--json) JSON=true; shift ;;
--html) HTML=true; shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done

mkdir -p "$REPORT_DIR"

# Detect stack
STACK="unknown"
if [[ -f "Cargo.toml" ]]; then STACK="rust"; fi
if [[ -f "package.json" ]]; then STACK="node"; fi
if [[ -f "pyproject.toml" || -f "setup.py" ]]; then STACK="python"; fi
if [[ -f "go.mod" ]]; then STACK="go"; fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stack detection order wrong

Medium Severity

Stack detection overwrites STACK with each manifest found, so the last match wins. A repo with both Cargo.toml and package.json (or go.mod) is graded with the wrong toolchain instead of a defined priority.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5117999. Configure here.

if [[ -f "Taskfile.yml" || -f "Justfile" ]]; then
: # already has task runner
fi

SCORE=0
MAX=0
CHECKS=()

run_check() {
local name="$1"
local cmd="$2"
local weight="${3:-1}"
local fast_skip="${4:-false}"

if [[ "$FAST" == true && "$fast_skip" == true ]]; then
CHECKS+=("{\"name\":\"$name\",\"status\":\"skipped\",\"score\":0,\"max\":$weight,\"detail\":\"skipped in fast mode\"}")
return 0
fi

MAX=$((MAX + weight))
if eval "$cmd" 2>&1 | tee "$REPORT_DIR/${name}.log" >"$REPORT_DIR/${name}.raw" 2>&1; then
SCORE=$((SCORE + weight))
CHECKS+=("{\"name\":\"$name\",\"status\":\"pass\",\"score\":$weight,\"max\":$weight,\"detail\":\"\"}")
echo " [PASS] $name"
else
local detail="$(head -5 "$REPORT_DIR/${name}.raw" | tr '\n' ' ')"
CHECKS+=("{\"name\":\"$name\",\"status\":\"fail\",\"score\":0,\"max\":$weight,\"detail\":\"$detail\"}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail detail breaks JSON

Medium Severity

Failed check details are pasted into JSON strings without escaping quotes or backslashes from command output. grade.sh --json can emit invalid JSON when a failing tool prints " or newlines in the first lines of its log.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5117999. Configure here.

Comment on lines +57 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the check output (detail) contains double quotes or backslashes, inserting it directly into the JSON string will produce malformed JSON and break the --json report generation. Sanitize the detail string by removing or escaping double quotes and backslashes.

Suggested change
local detail="$(head -5 "$REPORT_DIR/${name}.raw" | tr '\n' ' ')"
CHECKS+=("{\"name\":\"$name\",\"status\":\"fail\",\"score\":0,\"max\":$weight,\"detail\":\"$detail\"}")
local detail="$(head -5 "$REPORT_DIR/${name}.raw" | tr '\n' ' ' | tr -d '"' | tr -d '\\')"
CHECKS+=("{\"name\":\"$name\",\"status\":\"fail\",\"score\":0,\"max\":$weight,\"detail\":\"$detail\"}")

Comment on lines +57 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Failed-check log text is interpolated into JSON without escaping, so quotes/backslashes in command output can produce invalid JSON in grade.json. Serialize this field with proper JSON escaping (or use jq/a JSON encoder) before appending to CHECKS. [logic error]

Severity Level: Major ⚠️
- ❌ grade.json invalid when failed checks emit quotes/backslashes.
- ⚠️ Tooling consuming JSON reports fails or misbehaves.
Steps of Reproduction ✅
1. From the repo root `/workspace/phenoAI`, run `just grade-json` which is defined in
`justfile:19-21` to execute `./grade.sh --json`.

2. `grade.sh` detects the Rust stack (Cargo.toml present) and runs a series of checks via
`run_check()` at `grade.sh:40-61`; force one check to fail (for example, temporarily
introduce a syntax error in `crates/llm-router/src/lib.rs` so `cargo build --workspace`
fails with an error message containing double quotes).

3. On failure, `run_check()` captures the first 5 lines of the failing command's output
into `detail` at `grade.sh:57` using `head ... | tr '\n' ' '`, and appends a JSON fragment
to the `CHECKS` array at `grade.sh:58` by interpolating `$detail` directly inside
`"detail":"$detail"` without any JSON escaping.

4. When JSON output is requested, `grade.sh` writes `grade.json` at `grade.sh:141-155` by
joining the `CHECKS` entries; because `detail` can contain unescaped quotes/backslashes
from the tool output, `grade.json` becomes invalid JSON (e.g., `jq .
.grade-reports/grade.json` fails with a parse error).

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** grade.sh
**Line:** 57:58
**Comment:**
	*Logic Error: Failed-check log text is interpolated into JSON without escaping, so quotes/backslashes in command output can produce invalid JSON in `grade.json`. Serialize this field with proper JSON escaping (or use `jq`/a JSON encoder) before appending to `CHECKS`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

echo " [FAIL] $name"
fi
}

echo "========================================"
echo " GRADE — $(basename "$(pwd)")"
echo " Stack: $STACK"
echo " Mode: $([[ $FAST == true ]] && echo fast || echo full)"
echo "========================================"

case "$STACK" in
rust)
run_check "build" "cargo build --workspace" 2
run_check "test-unit" "cargo test --workspace" 3
run_check "fmt" "cargo fmt -- --check" 2
run_check "clippy" "cargo clippy --workspace --all-targets --all-features -- -D warnings" 2
run_check "deny" "cargo deny check" 1 true
run_check "doc" "cargo doc --workspace --no-deps" 1
run_check "test-snapshot" "cargo test --workspace -- snapshot" 1 true
run_check "test-fuzz" "cargo test --workspace -- fuzz" 1 true
run_check "coverage" "cargo llvm-cov --workspace --fail-under-lines 85" 2 true
run_check "audit" "cargo audit" 1 true
run_check "bench" "cargo bench --workspace" 1 true
;;
node)
run_check "install" "npm ci" 1
run_check "build" "npm run build" 2
run_check "test-unit" "npm test" 3
run_check "lint" "npx eslint . --ext .ts" 2
run_check "fmt" "npx prettier --check '**/*.ts'" 2
run_check "typecheck" "npx tsc --noEmit" 2
run_check "test-e2e" "npm run test:e2e" 2 true
run_check "test-perf" "npm run test:perf" 1 true
run_check "test-mutation" "npx stryker run" 1 true
run_check "coverage" "npx jest --coverage --coverageThreshold='{\"global\":{\"branches\":85,\"functions\":85,\"lines\":85,\"statements\":85}}'" 2 true
run_check "audit" "npm audit --audit-level=moderate" 1
;;
python)
run_check "install" "pip install -e '.[dev]'" 1
run_check "test-unit" "pytest -v" 3
run_check "lint" "ruff check src" 2
run_check "fmt" "ruff format --check src" 2
run_check "typecheck" "mypy src" 2
run_check "test-fuzz" "pytest -v --fuzz" 1 true
run_check "test-mutation" "mutmut run" 1 true
run_check "test-perf" "pytest -v --perf" 1 true
run_check "coverage" "pytest --cov=src --cov-report=term-missing --cov-fail-under=85" 2 true
run_check "security" "bandit -r src" 1
run_check "audit" "pip-audit" 1 true
;;
go)
run_check "build" "go build ./..." 2
run_check "test-unit" "go test ./..." 3
run_check "fmt" "test -z \"\$(gofmt -l .)\"" 2
run_check "vet" "go vet ./..." 2
run_check "lint" "golangci-lint run" 2
run_check "test-race" "go test -race ./..." 2 true
run_check "test-fuzz" "go test -fuzz=. ./..." 1 true
run_check "test-bench" "go test -bench=. ./..." 1 true
run_check "coverage" "go test -coverprofile=coverage.out -covermode=atomic ./... && go tool cover -func=coverage.out | grep total | awk '{print \$3}' | sed 's/%//' | awk '{exit(\$1 < 85 ? 1 : 0)}'" 2 true
run_check "audit" "govulncheck ./..." 1
;;
*)
echo "Unknown stack: $STACK"
exit 1
;;
esac

# Calculate percentage
PCT=$(( SCORE * 100 / MAX ))

# Determine grade
GRADE="F"
if [[ $PCT -ge 95 ]]; then GRADE="A+"; elif [[ $PCT -ge 90 ]]; then GRADE="A"; elif [[ $PCT -ge 85 ]]; then GRADE="B+"; elif [[ $PCT -ge 80 ]]; then GRADE="B"; elif [[ $PCT -ge 70 ]]; then GRADE="C"; elif [[ $PCT -ge 60 ]]; then GRADE="D"; fi

# Output summary
echo ""
echo "========================================"
echo " SCORE: $SCORE / $MAX ($PCT%)"
echo " GRADE: $GRADE"
echo "========================================"

# JSON output
if [[ "$JSON" == true ]]; then
cat > "$REPORT_DIR/grade.json" <<EOF
{
"project": "$(basename "$(pwd)")",
"stack": "$STACK",
"mode": "$([[ $FAST == true ]] && echo fast || echo full)",
"score": $SCORE,
"max": $MAX,
"percentage": $PCT,
"grade": "$GRADE",
"checks": [
$(IFS=,; echo "${CHECKS[*]}")
],
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
echo "JSON report: $REPORT_DIR/grade.json"
fi

# HTML output
if [[ "$HTML" == true ]]; then
cat > "$REPORT_DIR/grade.html" <<EOF
<!DOCTYPE html>
<html>
<head><title>Grade Report — $(basename "$(pwd)")</title>
<style>
body{font-family:system-ui,sans-serif;max-width:800px;margin:2rem auto;padding:0 1rem}
h1{border-bottom:2px solid #333}
.score{font-size:3rem;font-weight:bold;color:$([[ $PCT -ge 85 ]] && echo "#2d7" || echo "#d42")}
.grade{font-size:1.5rem}
table{width:100%;border-collapse:collapse;margin:1rem 0}
th,td{padding:0.5rem;text-align:left;border-bottom:1px solid #ddd}
th{background:#f5f5f5}
.pass{color:#2d7}.fail{color:#d42}.skip{color:#888}
</style>
</head>
<body>
<h1>Grade Report — $(basename "$(pwd)")</h1>
<p class="score">$PCT% <span class="grade">($GRADE)</span></p>
<p>Stack: $STACK | Mode: $([[ $FAST == true ]] && echo fast || echo full)</p>
<table>
<tr><th>Check</th><th>Status</th><th>Score</th></tr>
EOF
for check in "${CHECKS[@]}"; do
name=$(echo "$check" | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
status=$(echo "$check" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
score=$(echo "$check" | grep -o '"score":[0-9]*' | cut -d':' -f2)
max=$(echo "$check" | grep -o '"max":[0-9]*' | cut -d':' -f2)
Comment on lines +186 to +189

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Parsing JSON with a simple grep -o and cut is fragile because if the detail field contains keys like "name":"..." or "status":"..." (e.g., from compiler/test outputs), grep will match multiple lines and produce garbled HTML. Use anchored sed patterns to extract the fields robustly from the start of the JSON string.

Suggested change
name=$(echo "$check" | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
status=$(echo "$check" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
score=$(echo "$check" | grep -o '"score":[0-9]*' | cut -d':' -f2)
max=$(echo "$check" | grep -o '"max":[0-9]*' | cut -d':' -f2)
name=$(echo "$check" | sed -E 's/^\{"name":"([^"]*)".*/\1/')
status=$(echo "$check" | sed -E 's/^\{"name":"[^"]*","status":"([^"]*)".*/\1/')
score=$(echo "$check" | sed -E 's/^\{"name":"[^"]*","status":"[^"]*","score":([0-9]+).*/\1/')
max=$(echo "$check" | sed -E 's/^\{"name":"[^"]*","status":"[^"]*","score":[0-9]+,"max":([0-9]+).*/\1/')

echo "<tr><td>$name</td><td class=\"$status\">$status</td><td>$score/$max</td></tr>" >> "$REPORT_DIR/grade.html"
done
echo "</table><p>Generated: $(date -u)</p></body></html>" >> "$REPORT_DIR/grade.html"
echo "HTML report: $REPORT_DIR/grade.html"
fi

# Exit code
if [[ $PCT -lt 85 ]]; then exit 1; fi
exit 0
Loading
Loading