feat(who): configurable scoring weights, MR refs, detail mode, and suffix path resolution
Expert mode now surfaces the specific MR references (project/path!iid) that contributed to each expert's score, capped at 50 per user. A new --detail flag adds per-MR breakdowns showing role (Author/Reviewer/both), note count, and last activity timestamp. Scoring weights (author_weight, reviewer_weight, note_bonus) are now configurable via the config file's `scoring` section with validation that rejects negative values. Defaults shift to author_weight=25, reviewer_weight=10, note_bonus=1 — better reflecting that code authorship is a stronger expertise signal than review assignment alone. Path resolution gains suffix matching: typing "login.rs" auto-resolves to "src/auth/login.rs" when unambiguous, with clear disambiguation errors when multiple paths match. Project-scoping (-p) narrows the candidate set. The MAX_MR_REFS_PER_USER constant is promoted to module scope for reuse across expert and overlap modes. Human output shows MR refs inline and detail sub-rows when requested. Robot JSON includes mr_refs, mr_refs_total, mr_refs_truncated, and optional details array. Includes comprehensive tests for suffix resolution, scoring weight configurability, MR ref aggregation across projects, and detail mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
50
AGENTS.md
50
AGENTS.md
@@ -740,3 +740,53 @@ lore -J mrs --fields iid,title,state,draft,labels # Custom field list
|
|||||||
- Use `lore --robot health` as a fast pre-flight check before queries
|
- Use `lore --robot health` as a fast pre-flight check before queries
|
||||||
- Use `lore robot-docs` for response schema discovery
|
- Use `lore robot-docs` for response schema discovery
|
||||||
- The `-p` flag supports fuzzy project matching (suffix and substring)
|
- The `-p` flag supports fuzzy project matching (suffix and substring)
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
## UBS Quick Reference for AI Agents
|
||||||
|
|
||||||
|
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||||
|
|
||||||
|
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||||
|
|
||||||
|
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
```bash
|
||||||
|
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||||
|
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||||
|
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||||
|
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||||
|
ubs --help # Full command reference
|
||||||
|
ubs sessions --entries 1 # Tail the latest install session log
|
||||||
|
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Format:**
|
||||||
|
```
|
||||||
|
⚠️ Category (N errors)
|
||||||
|
file.ts:42:5 – Issue description
|
||||||
|
💡 Suggested fix
|
||||||
|
Exit code: 1
|
||||||
|
```
|
||||||
|
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||||
|
|
||||||
|
**Fix Workflow:**
|
||||||
|
1. Read finding → category + fix suggestion
|
||||||
|
2. Navigate `file:line:col` → view context
|
||||||
|
3. Verify real issue (not false positive)
|
||||||
|
4. Fix root cause (not symptom)
|
||||||
|
5. Re-run `ubs <file>` → exit 0
|
||||||
|
6. Commit
|
||||||
|
|
||||||
|
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||||
|
|
||||||
|
**Bug Severity:**
|
||||||
|
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||||
|
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||||
|
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||||
|
|
||||||
|
**Anti-Patterns:**
|
||||||
|
- ❌ Ignore findings → ✅ Investigate each
|
||||||
|
- ❌ Full scan per edit → ✅ Scope to file
|
||||||
|
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||||
|
````
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--since",
|
"--since",
|
||||||
"--project",
|
"--project",
|
||||||
"--limit",
|
"--limit",
|
||||||
|
"--detail",
|
||||||
|
"--no-detail",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -751,6 +751,13 @@ pub struct WhoArgs {
|
|||||||
help_heading = "Output"
|
help_heading = "Output"
|
||||||
)]
|
)]
|
||||||
pub limit: u16,
|
pub limit: u16,
|
||||||
|
|
||||||
|
/// Show per-MR detail breakdown (expert mode only)
|
||||||
|
#[arg(long, help_heading = "Output", overrides_with = "no_detail")]
|
||||||
|
pub detail: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-detail", hide = true, overrides_with = "detail")]
|
||||||
|
pub no_detail: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|||||||
@@ -146,6 +146,32 @@ impl Default for LoggingConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ScoringConfig {
|
||||||
|
/// Points per MR where the user authored code touching the path.
|
||||||
|
#[serde(rename = "authorWeight")]
|
||||||
|
pub author_weight: i64,
|
||||||
|
|
||||||
|
/// Points per MR where the user reviewed code touching the path.
|
||||||
|
#[serde(rename = "reviewerWeight")]
|
||||||
|
pub reviewer_weight: i64,
|
||||||
|
|
||||||
|
/// Bonus points per individual inline review comment (DiffNote).
|
||||||
|
#[serde(rename = "noteBonus")]
|
||||||
|
pub note_bonus: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScoringConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
author_weight: 25,
|
||||||
|
reviewer_weight: 10,
|
||||||
|
note_bonus: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub gitlab: GitLabConfig,
|
pub gitlab: GitLabConfig,
|
||||||
@@ -162,6 +188,9 @@ pub struct Config {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub logging: LoggingConfig,
|
pub logging: LoggingConfig,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub scoring: ScoringConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -207,10 +236,31 @@ impl Config {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_scoring(&config.scoring)?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_scoring(scoring: &ScoringConfig) -> Result<()> {
|
||||||
|
if scoring.author_weight < 0 {
|
||||||
|
return Err(LoreError::ConfigInvalid {
|
||||||
|
details: "scoring.authorWeight must be >= 0".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if scoring.reviewer_weight < 0 {
|
||||||
|
return Err(LoreError::ConfigInvalid {
|
||||||
|
details: "scoring.reviewerWeight must be >= 0".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if scoring.note_bonus < 0 {
|
||||||
|
return Err(LoreError::ConfigInvalid {
|
||||||
|
details: "scoring.noteBonus must be >= 0".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
#[derive(Debug, serde::Serialize)]
|
||||||
pub struct MinimalConfig {
|
pub struct MinimalConfig {
|
||||||
pub gitlab: MinimalGitLabConfig,
|
pub gitlab: MinimalGitLabConfig,
|
||||||
@@ -236,3 +286,81 @@ impl serde::Serialize for ProjectConfig {
|
|||||||
state.end()
|
state.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn write_config(dir: &TempDir, scoring_json: &str) -> std::path::PathBuf {
|
||||||
|
let path = dir.path().join("config.json");
|
||||||
|
let config = format!(
|
||||||
|
r#"{{
|
||||||
|
"gitlab": {{
|
||||||
|
"baseUrl": "https://gitlab.example.com",
|
||||||
|
"tokenEnvVar": "GITLAB_TOKEN"
|
||||||
|
}},
|
||||||
|
"projects": [
|
||||||
|
{{ "path": "group/project" }}
|
||||||
|
],
|
||||||
|
"scoring": {scoring_json}
|
||||||
|
}}"#
|
||||||
|
);
|
||||||
|
fs::write(&path, config).unwrap();
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_rejects_negative_author_weight() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = write_config(
|
||||||
|
&dir,
|
||||||
|
r#"{
|
||||||
|
"authorWeight": -1,
|
||||||
|
"reviewerWeight": 10,
|
||||||
|
"noteBonus": 1
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
let err = Config::load_from_path(&path).unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("scoring.authorWeight"),
|
||||||
|
"unexpected error: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_rejects_negative_reviewer_weight() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = write_config(
|
||||||
|
&dir,
|
||||||
|
r#"{
|
||||||
|
"authorWeight": 25,
|
||||||
|
"reviewerWeight": -1,
|
||||||
|
"noteBonus": 1
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
let err = Config::load_from_path(&path).unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("scoring.reviewerWeight"),
|
||||||
|
"unexpected error: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_rejects_negative_note_bonus() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = write_config(
|
||||||
|
&dir,
|
||||||
|
r#"{
|
||||||
|
"authorWeight": 25,
|
||||||
|
"reviewerWeight": 10,
|
||||||
|
"noteBonus": -1
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
let err = Config::load_from_path(&path).unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(msg.contains("scoring.noteBonus"), "unexpected error: {msg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user