- Implemented a code quality scanner that checks for common clean-code violations in Rust source files, including missing documentation, usage of `.unwrap()` in production code, and commented-out code. - Introduced a commit message validator that follows the Conventional Commits specification, ensuring proper formatting and providing suggestions for invalid messages. - Created a unified BestPracticeEngine to encapsulate the functionalities of skills, architecture audits, code quality checks, and commit message validation. - Added tests for both the code quality scanner and commit message validator to ensure reliability and correctness.
340 lines
14 KiB
Rust
340 lines
14 KiB
Rust
//! Built-in best-practice tools for zesdex.
|
|
//!
|
|
//! These tools allow the LLM agent to run architecture audits, code-quality
|
|
//! scans, commit-message validation, and skill lookups — all as compiled-in
|
|
//! features of the zesdex binary.
|
|
//!
|
|
//! # Tools
|
|
//!
|
|
//! | Tool name | Action |
|
|
//! |-----------|--------|
|
|
//! | `best_practice` | Run architecture audit, code-quality scan, or skill lookup |
|
|
//! | `commit_convention` | Validate or suggest commit messages |
|
|
|
|
use crate::best_practice::BestPracticeEngine;
|
|
use crate::tools::{arg_str, Tool, ToolCtx};
|
|
use anyhow::{bail, Result};
|
|
use serde_json::{json, Value};
|
|
use std::sync::OnceLock;
|
|
use tracing::instrument;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared engine instance (lazy, created once)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn engine() -> &'static BestPracticeEngine {
|
|
static ENGINE: OnceLock<BestPracticeEngine> = OnceLock::new();
|
|
ENGINE.get_or_init(BestPracticeEngine::new)
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
// best_practice
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Run best-practice audits (architecture, code quality, skills).
|
|
pub struct BestPractice;
|
|
|
|
impl Tool for BestPractice {
|
|
fn name(&self) -> &'static str {
|
|
"best_practice"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Run architecture audit, code-quality scan, embedded-skills lookup, \
|
|
or commit-message validation. Sub-actions: 'audit_all', 'audit_layering', \
|
|
'scan_quality', 'list_skills', 'get_skill', 'validate_commit', 'suggest_commit'."
|
|
}
|
|
|
|
fn parameters(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"action": {
|
|
"type": "string",
|
|
"enum": [
|
|
"audit_all",
|
|
"audit_layering",
|
|
"scan_quality",
|
|
"list_skills",
|
|
"get_skill",
|
|
"validate_commit",
|
|
"suggest_commit"
|
|
],
|
|
"description": "Which action to perform"
|
|
},
|
|
"workspace": {
|
|
"type": "string",
|
|
"description": "Path to workspace root (required for audit/scan actions)"
|
|
},
|
|
"skill_name": {
|
|
"type": "string",
|
|
"description": "Skill name to retrieve (required for 'get_skill')"
|
|
},
|
|
"commit_message": {
|
|
"type": "string",
|
|
"description": "Commit message to validate (required for 'validate_commit')"
|
|
},
|
|
"commit_type": {
|
|
"type": "string",
|
|
"description": "Commit type for template suggestion (e.g. 'feat', 'fix')"
|
|
},
|
|
"commit_scope": {
|
|
"type": "string",
|
|
"description": "Optional scope for template suggestion"
|
|
}
|
|
},
|
|
"required": ["action"]
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, _ctx, args))]
|
|
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
let action = arg_str(args, "action")?;
|
|
let eng = engine();
|
|
|
|
match action.as_str() {
|
|
"list_skills" => {
|
|
let skills = eng.list_skills();
|
|
let summaries = eng.skill_summaries();
|
|
let mut out = String::from("=== Embedded Best-Practice Skills ===\n\n");
|
|
for (name, desc) in &summaries {
|
|
out.push_str(&format!(" {name:<40} {desc}\n"));
|
|
}
|
|
if skills.is_empty() {
|
|
out.push_str(" (no skills embedded)\n");
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
"get_skill" => {
|
|
let name = arg_str(args, "skill_name")?;
|
|
match eng.get_skill(&name) {
|
|
Some(content) => Ok(content.to_string()),
|
|
None => {
|
|
let available = eng.list_skills();
|
|
bail!(
|
|
"Skill '{name}' not found. Available skills: {}",
|
|
available.join(", ")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
"validate_commit" => {
|
|
let msg = arg_str(args, "commit_message")?;
|
|
match eng.validate_commit(&msg) {
|
|
Ok(()) => Ok("✅ Commit message is valid.".to_string()),
|
|
Err(errors) => {
|
|
let mut out = String::from("❌ Commit message validation failed:\n");
|
|
for err in &errors {
|
|
out.push_str(&format!(" - {err}\n"));
|
|
}
|
|
// Suggest a template.
|
|
if let Some(parsed) = eng.parse_commit(&msg) {
|
|
let tpl = eng.suggest_commit_template(&parsed.type_, parsed.scope.as_deref());
|
|
out.push_str(&format!("\nTemplate: {tpl}\n"));
|
|
}
|
|
Ok(out)
|
|
}
|
|
}
|
|
}
|
|
|
|
"suggest_commit" => {
|
|
let type_ = arg_str(args, "commit_type")?;
|
|
let scope = args.get("commit_scope").and_then(|v| v.as_str());
|
|
let tpl = eng.suggest_commit_template(&type_, scope);
|
|
Ok(format!(
|
|
"Suggested commit template:\n\n {tpl}\n\n\
|
|
Valid types: feat, fix, chore, docs, refactor, test, style, perf, ci, build, revert"
|
|
))
|
|
}
|
|
|
|
"audit_layering" | "audit_all" | "scan_quality" => {
|
|
let workspace = match args.get("workspace").and_then(|v| v.as_str()) {
|
|
Some(w) => w.to_string(),
|
|
None => bail!("'workspace' argument is required for '{action}'"),
|
|
};
|
|
let ws_path = std::path::Path::new(&workspace);
|
|
|
|
// Use the host's dirs data dir when workspace is the default data directory.
|
|
let report = match action.as_str() {
|
|
"audit_layering" => {
|
|
let r = eng.audit_layering(ws_path)?;
|
|
let mut out = format!(
|
|
"=== Architecture Layering Audit ===\n\
|
|
Files scanned: {}\n",
|
|
r.files_scanned
|
|
);
|
|
if r.violations.is_empty() {
|
|
out.push_str(" ✅ No layering violations found.\n");
|
|
} else {
|
|
out.push_str(&format!(" Errors: {}\n", r.error_count()));
|
|
out.push_str(&format!(" Warnings: {}\n", r.warning_count()));
|
|
for v in &r.violations {
|
|
out.push_str(&format!(
|
|
" [{}] {}:{} — {}\n",
|
|
v.severity, v.file, v.line, v.message
|
|
));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
"scan_quality" => {
|
|
let r = eng.scan_quality(ws_path)?;
|
|
let mut out = format!(
|
|
"=== Code Quality Scan ===\n\
|
|
Files scanned: {}\n",
|
|
r.files_scanned
|
|
);
|
|
if r.findings.is_empty() {
|
|
out.push_str(" ✅ No code-quality issues found.\n");
|
|
} else {
|
|
let by_rule = r.count_by_rule();
|
|
out.push_str(" By rule:\n");
|
|
for (rule, count) in &by_rule {
|
|
out.push_str(&format!(" {rule}: {count}\n"));
|
|
}
|
|
for f in r.findings.iter().take(15) {
|
|
out.push_str(&format!(
|
|
" [{}] {}:{} — {}: {}\n",
|
|
f.severity, f.file, f.line, f.rule, f.message
|
|
));
|
|
}
|
|
if r.findings.len() > 15 {
|
|
out.push_str(&format!(
|
|
" ... and {} more findings.\n",
|
|
r.findings.len() - 15
|
|
));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
_ => {
|
|
let combined = eng.audit_all(ws_path)?;
|
|
combined.format()
|
|
}
|
|
};
|
|
Ok(report)
|
|
}
|
|
|
|
other => bail!("Unknown action '{other}'"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
// commit_convention
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Dedicated commit-message validation tool.
|
|
pub struct CommitConvention;
|
|
|
|
impl Tool for CommitConvention {
|
|
fn name(&self) -> &'static str {
|
|
"commit_convention"
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Validate a git commit message against Conventional Commits format (Bahasa Indonesia). \
|
|
Checks type, scope, description casing, length, and punctuation."
|
|
}
|
|
|
|
fn parameters(&self) -> Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {
|
|
"type": "string",
|
|
"description": "The full commit message to validate"
|
|
}
|
|
},
|
|
"required": ["message"]
|
|
})
|
|
}
|
|
|
|
#[instrument(skip(self, _ctx, args))]
|
|
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
let msg = arg_str(args, "message")?;
|
|
let eng = engine();
|
|
|
|
// Try to parse for additional context.
|
|
let info = eng.parse_commit(&msg);
|
|
|
|
match eng.validate_commit(&msg) {
|
|
Ok(()) => {
|
|
let mut out = String::from("✅ Valid Conventional Commit.\n");
|
|
if let Some(i) = info {
|
|
out.push_str(&format!(" Type: {}\n", i.type_));
|
|
if let Some(ref s) = i.scope {
|
|
out.push_str(&format!(" Scope: {s}\n"));
|
|
}
|
|
out.push_str(&format!(" Breaking: {}\n", i.breaking));
|
|
out.push_str(&format!(" Description: {}\n", i.description));
|
|
}
|
|
Ok(out)
|
|
}
|
|
Err(errors) => {
|
|
let mut out = String::from("❌ Invalid commit message:\n");
|
|
for err in &errors {
|
|
out.push_str(&format!(" - {err}\n"));
|
|
}
|
|
// Provide a template suggestion.
|
|
out.push_str("\nExpected format:\n");
|
|
out.push_str(" feat(scope): <imperative description>\n");
|
|
out.push_str(" fix(scope): <imperative description>\n");
|
|
out.push_str(" chore: <imperative description>\n");
|
|
out.push_str(" docs: <imperative description>\n");
|
|
Ok(out)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn test_ctx() -> ToolCtx {
|
|
// Minimal context for unit tests
|
|
let temp = std::env::temp_dir().join("zesdex-bptest");
|
|
let _ = std::fs::create_dir_all(&temp);
|
|
ToolCtx::builder()
|
|
.workspaces(vec![temp.clone()])
|
|
.session_dir(temp.clone())
|
|
.build()
|
|
}
|
|
|
|
#[test]
|
|
fn best_practice_list_skills() {
|
|
let tool = BestPractice;
|
|
let args = json!({"action": "list_skills"});
|
|
let result = tool.run(&test_ctx(), &args).unwrap();
|
|
assert!(result.contains("clean-code"), "should list clean-code: {result}");
|
|
assert!(result.contains("commit-convention"), "should list commit-convention: {result}");
|
|
}
|
|
|
|
#[test]
|
|
fn best_practice_get_skill() {
|
|
let tool = BestPractice;
|
|
let args = json!({"action": "get_skill", "skill_name": "clean-code"});
|
|
let result = tool.run(&test_ctx(), &args).unwrap();
|
|
assert!(result.contains("Clean Code"), "should contain skill content: {result}");
|
|
}
|
|
|
|
#[test]
|
|
fn commit_convention_valid() {
|
|
let tool = CommitConvention;
|
|
let args = json!({"message": "feat(tool): add best practice audit"});
|
|
let result = tool.run(&test_ctx(), &args).unwrap();
|
|
assert!(result.contains("✅"), "valid commit should succeed: {result}");
|
|
}
|
|
|
|
#[test]
|
|
fn commit_convention_invalid() {
|
|
let tool = CommitConvention;
|
|
let args = json!({"message": "Add new feature"});
|
|
let result = tool.run(&test_ctx(), &args).unwrap();
|
|
assert!(result.contains("❌"), "invalid commit should fail: {result}");
|
|
}
|
|
}
|