Skip to content

Commit 98b509f

Browse files
authored
feat: add triage command — composite risk audit queue (#224)
* feat: add triage command — composite risk audit queue Merges connectivity (fan-in), complexity (cognitive), churn (commit count), role classification, and maintainability index into a single weighted risk score. Ranks symbols to surface the highest-risk areas for audit. Scoring: riskScore = w.fanIn×norm(fan_in) + w.complexity×norm(cognitive) + w.churn×norm(churn) + w.role×ROLE_WEIGHTS[role] + w.mi×(1−norm(MI)) Supports --sort (risk|complexity|churn|fan-in|mi), --min-score, --role, --file, --kind, --no-tests, --weights JSON, pagination, JSON/NDJSON output. Exposed as CLI command and MCP tool. Impact: 6 functions changed, 5 affected * fix: log SQL errors in triage catch block instead of silencing them Impact: 1 functions changed, 1 affected * fix: add triage to MCP test tool name list
1 parent f35c797 commit 98b509f

File tree

7 files changed

+699
-0
lines changed

7 files changed

+699
-0
lines changed

src/cli.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,58 @@ program
10351035
});
10361036
});
10371037

1038+
program
1039+
.command('triage')
1040+
.description(
1041+
'Ranked audit queue by composite risk score (connectivity + complexity + churn + role)',
1042+
)
1043+
.option('-d, --db <path>', 'Path to graph.db')
1044+
.option('-n, --limit <number>', 'Max results to return', '20')
1045+
.option('--sort <metric>', 'Sort metric: risk | complexity | churn | fan-in | mi', 'risk')
1046+
.option('--min-score <score>', 'Only show symbols with risk score >= threshold')
1047+
.option('--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)')
1048+
.option('-f, --file <path>', 'Scope to a specific file (partial match)')
1049+
.option('-k, --kind <kind>', 'Filter by symbol kind (function, method, class)')
1050+
.option('-T, --no-tests', 'Exclude test/spec files from results')
1051+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1052+
.option('-j, --json', 'Output as JSON')
1053+
.option('--offset <number>', 'Skip N results (default: 0)')
1054+
.option('--ndjson', 'Newline-delimited JSON output')
1055+
.option('--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')')
1056+
.action(async (opts) => {
1057+
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
1058+
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
1059+
process.exit(1);
1060+
}
1061+
if (opts.role && !VALID_ROLES.includes(opts.role)) {
1062+
console.error(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
1063+
process.exit(1);
1064+
}
1065+
let weights;
1066+
if (opts.weights) {
1067+
try {
1068+
weights = JSON.parse(opts.weights);
1069+
} catch {
1070+
console.error('Invalid --weights JSON');
1071+
process.exit(1);
1072+
}
1073+
}
1074+
const { triage } = await import('./triage.js');
1075+
triage(opts.db, {
1076+
limit: parseInt(opts.limit, 10),
1077+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1078+
sort: opts.sort,
1079+
minScore: opts.minScore,
1080+
role: opts.role,
1081+
file: opts.file,
1082+
kind: opts.kind,
1083+
noTests: resolveNoTests(opts),
1084+
json: opts.json,
1085+
ndjson: opts.ndjson,
1086+
weights,
1087+
});
1088+
});
1089+
10381090
program
10391091
.command('owners [target]')
10401092
.description('Show CODEOWNERS mapping for files and functions')

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,7 @@ export {
141141
moduleBoundariesData,
142142
structureData,
143143
} from './structure.js';
144+
// Triage — composite risk audit
145+
export { triage, triageData } from './triage.js';
144146
// Watch mode
145147
export { watchProject } from './watcher.js';

src/mcp.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,43 @@ const BASE_TOOLS = [
599599
required: ['command', 'targets'],
600600
},
601601
},
602+
{
603+
name: 'triage',
604+
description:
605+
'Ranked audit queue by composite risk score. Merges connectivity (fan-in), complexity (cognitive), churn (commit count), role classification, and maintainability index into a single weighted score.',
606+
inputSchema: {
607+
type: 'object',
608+
properties: {
609+
sort: {
610+
type: 'string',
611+
enum: ['risk', 'complexity', 'churn', 'fan-in', 'mi'],
612+
description: 'Sort metric (default: risk)',
613+
},
614+
min_score: {
615+
type: 'number',
616+
description: 'Only return symbols with risk score >= this threshold (0-1)',
617+
},
618+
role: {
619+
type: 'string',
620+
enum: VALID_ROLES,
621+
description: 'Filter by role classification',
622+
},
623+
file: { type: 'string', description: 'Scope to file (partial match)' },
624+
kind: {
625+
type: 'string',
626+
enum: ['function', 'method', 'class'],
627+
description: 'Filter by symbol kind',
628+
},
629+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
630+
weights: {
631+
type: 'object',
632+
description:
633+
'Custom scoring weights (e.g. {"fanIn":1,"complexity":0,"churn":0,"role":0,"mi":0})',
634+
},
635+
...PAGINATION_PROPS,
636+
},
637+
},
638+
},
602639
{
603640
name: 'branch_compare',
604641
description:
@@ -1091,6 +1128,21 @@ export async function startMCPServer(customDbPath, options = {}) {
10911128
});
10921129
break;
10931130
}
1131+
case 'triage': {
1132+
const { triageData } = await import('./triage.js');
1133+
result = triageData(dbPath, {
1134+
sort: args.sort,
1135+
minScore: args.min_score,
1136+
role: args.role,
1137+
file: args.file,
1138+
kind: args.kind,
1139+
noTests: args.no_tests,
1140+
weights: args.weights,
1141+
limit: Math.min(args.limit ?? MCP_DEFAULTS.triage, MCP_MAX_LIMIT),
1142+
offset: args.offset ?? 0,
1143+
});
1144+
break;
1145+
}
10941146
case 'branch_compare': {
10951147
const { branchCompareData, branchCompareMermaid } = await import('./branch-compare.js');
10961148
const bcData = await branchCompareData(args.base, args.target, {

src/paginate.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const MCP_DEFAULTS = {
3030
manifesto: 50,
3131
communities: 20,
3232
structure: 30,
33+
triage: 20,
3334
};
3435

3536
/** Hard cap to prevent abuse via MCP. */

0 commit comments

Comments
 (0)