- Fixed unused function parameters by prefixing with underscore - Removed unused imports and variables - Applied eslint --fix for automatic style fixes - Property shorthand - String template literals - Prefer const over let where appropriate - Spacing and formatting Reduces lint errors from 108+ to 78 (61 unused vars, 17 other issues) Related to CI lint failures in previous commit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
/**
|
|
* Rule Optimizer Service
|
|
*
|
|
* Analyzes governance rules for clarity, specificity, and actionability.
|
|
* Provides optimization suggestions to improve rule quality.
|
|
*
|
|
* Part of Phase 2: AI Rule Optimizer & CLAUDE.md Analyzer
|
|
*/
|
|
|
|
class RuleOptimizer {
|
|
constructor() {
|
|
// Weak language that reduces rule clarity
|
|
this.weakWords = [
|
|
'try', 'maybe', 'consider', 'might', 'probably',
|
|
'possibly', 'perhaps', 'could', 'should'
|
|
];
|
|
|
|
// Strong imperatives that improve clarity
|
|
this.strongWords = [
|
|
'MUST', 'SHALL', 'REQUIRED', 'PROHIBITED',
|
|
'NEVER', 'ALWAYS', 'MANDATORY'
|
|
];
|
|
|
|
// Hedging phrases that reduce actionability
|
|
this.hedgingPhrases = [
|
|
'if possible', 'when convenient', 'as needed',
|
|
'try to', 'attempt to', 'ideally'
|
|
];
|
|
|
|
// Vague terms that reduce specificity
|
|
this.vagueTerms = [
|
|
'this project', 'the system', 'the application',
|
|
'things', 'stuff', 'appropriately', 'properly',
|
|
'correctly', 'efficiently'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Analyze rule clarity (0-100)
|
|
*
|
|
* Measures how unambiguous and clear the rule is.
|
|
* Penalizes weak language, rewards strong imperatives.
|
|
*
|
|
* @param {string} ruleText - The rule text to analyze
|
|
* @returns {Object} { score: number, issues: string[], strengths: string[] }
|
|
*/
|
|
analyzeClarity(ruleText) {
|
|
let score = 100;
|
|
const issues = [];
|
|
const strengths = [];
|
|
|
|
// Check for weak words (heavy penalty)
|
|
this.weakWords.forEach(word => {
|
|
const regex = new RegExp(`\\b${word}\\b`, 'i');
|
|
if (regex.test(ruleText)) {
|
|
score -= 15;
|
|
issues.push(`Weak language detected: "${word}" - reduces clarity`);
|
|
}
|
|
});
|
|
|
|
// Check for strong imperatives (bonus)
|
|
const hasStrong = this.strongWords.some(word =>
|
|
new RegExp(`\\b${word}\\b`).test(ruleText)
|
|
);
|
|
if (hasStrong) {
|
|
strengths.push('Uses strong imperative language (MUST, SHALL, etc.)');
|
|
} else {
|
|
score -= 10;
|
|
issues.push('No strong imperatives found - add MUST, SHALL, REQUIRED, etc.');
|
|
}
|
|
|
|
// Check for hedging phrases
|
|
this.hedgingPhrases.forEach(phrase => {
|
|
if (ruleText.toLowerCase().includes(phrase)) {
|
|
score -= 10;
|
|
issues.push(`Hedging detected: "${phrase}" - reduces certainty`);
|
|
}
|
|
});
|
|
|
|
// Check for specificity indicators
|
|
const hasVariables = /\$\{[A-Z_]+\}/.test(ruleText);
|
|
const hasNumbers = /\d/.test(ruleText);
|
|
|
|
if (hasVariables) {
|
|
strengths.push('Uses variables for parameterization');
|
|
} else if (!hasNumbers) {
|
|
score -= 10;
|
|
issues.push('No specific parameters (numbers or variables) - add concrete values');
|
|
}
|
|
|
|
// Check for context (WHO, WHAT, WHERE)
|
|
if (ruleText.length < 20) {
|
|
score -= 15;
|
|
issues.push('Too brief - lacks context (WHO, WHAT, WHEN, WHERE)');
|
|
}
|
|
|
|
return {
|
|
score: Math.max(0, Math.min(100, score)),
|
|
issues,
|
|
strengths
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyze rule specificity (0-100)
|
|
*
|
|
* Measures how concrete and specific the rule is.
|
|
* Rewards explicit parameters, penalizes vague terms.
|
|
*
|
|
* @param {string} ruleText - The rule text to analyze
|
|
* @returns {Object} { score: number, issues: string[], strengths: string[] }
|
|
*/
|
|
analyzeSpecificity(ruleText) {
|
|
let score = 100;
|
|
const issues = [];
|
|
const strengths = [];
|
|
|
|
// Check for vague terms
|
|
this.vagueTerms.forEach(term => {
|
|
if (ruleText.toLowerCase().includes(term)) {
|
|
score -= 12;
|
|
issues.push(`Vague term: "${term}" - be more specific`);
|
|
}
|
|
});
|
|
|
|
// Check for concrete parameters
|
|
const hasVariables = /\$\{[A-Z_]+\}/.test(ruleText);
|
|
const hasNumbers = /\d+/.test(ruleText);
|
|
const hasPaths = /[\/\\][\w\/\\-]+/.test(ruleText);
|
|
const hasUrls = /https?:\/\//.test(ruleText);
|
|
const hasFilenames = /\.\w{2,4}/.test(ruleText);
|
|
|
|
let specificityCount = 0;
|
|
if (hasVariables) {
|
|
strengths.push('Includes variables for parameterization');
|
|
specificityCount++;
|
|
}
|
|
if (hasNumbers) {
|
|
strengths.push('Includes specific numbers (ports, counts, etc.)');
|
|
specificityCount++;
|
|
}
|
|
if (hasPaths) {
|
|
strengths.push('Includes file paths or directories');
|
|
specificityCount++;
|
|
}
|
|
if (hasUrls) {
|
|
strengths.push('Includes URLs or domains');
|
|
specificityCount++;
|
|
}
|
|
if (hasFilenames) {
|
|
strengths.push('Includes specific filenames');
|
|
specificityCount++;
|
|
}
|
|
|
|
if (specificityCount === 0) {
|
|
score -= 20;
|
|
issues.push('No concrete parameters - add numbers, paths, variables, or URLs');
|
|
}
|
|
|
|
// Check for examples (inline)
|
|
if (ruleText.includes('e.g.') || ruleText.includes('example:')) {
|
|
strengths.push('Includes examples for clarification');
|
|
}
|
|
|
|
return {
|
|
score: Math.max(0, Math.min(100, score)),
|
|
issues,
|
|
strengths
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyze rule actionability (0-100)
|
|
*
|
|
* Measures how clearly the rule can be acted upon.
|
|
* Checks for WHO, WHAT, WHEN, WHERE elements.
|
|
*
|
|
* @param {string} ruleText - The rule text to analyze
|
|
* @returns {Object} { score: number, issues: string[], strengths: string[] }
|
|
*/
|
|
analyzeActionability(ruleText) {
|
|
let score = 100;
|
|
const issues = [];
|
|
const strengths = [];
|
|
|
|
// Check for action elements
|
|
const hasSubject = /\b(all|every|any|developer|user|system|database|api|file)\b/i.test(ruleText);
|
|
const hasAction = /\b(use|create|delete|update|set|configure|deploy|test|validate)\b/i.test(ruleText);
|
|
const hasObject = /\b(port|database|file|config|environment|variable|parameter)\b/i.test(ruleText);
|
|
|
|
if (hasSubject) {
|
|
strengths.push('Specifies WHO/WHAT should act');
|
|
} else {
|
|
score -= 15;
|
|
issues.push('Missing subject - specify WHO or WHAT (e.g., "Database", "All files")');
|
|
}
|
|
|
|
if (hasAction) {
|
|
strengths.push('Includes clear action verb');
|
|
} else {
|
|
score -= 15;
|
|
issues.push('Missing action verb - specify WHAT TO DO (e.g., "use", "create", "delete")');
|
|
}
|
|
|
|
if (hasObject) {
|
|
strengths.push('Specifies target object');
|
|
} else {
|
|
score -= 10;
|
|
issues.push('Missing object - specify the target (e.g., "port 27017", "config file")');
|
|
}
|
|
|
|
// Check for conditions (IF-THEN structure)
|
|
const hasConditional = /\b(if|when|unless|while|during|before|after)\b/i.test(ruleText);
|
|
if (hasConditional) {
|
|
strengths.push('Includes conditional logic (IF-THEN)');
|
|
}
|
|
|
|
// Check for exceptions (BUT, EXCEPT, UNLESS)
|
|
const hasException = /\b(except|unless|but|excluding)\b/i.test(ruleText);
|
|
if (hasException) {
|
|
strengths.push('Defines exceptions or boundary conditions');
|
|
}
|
|
|
|
// Check for measurability
|
|
const hasMeasurableOutcome = /\b(all|every|zero|100%|always|never|exactly)\b/i.test(ruleText);
|
|
if (hasMeasurableOutcome) {
|
|
strengths.push('Includes measurable outcome');
|
|
} else {
|
|
score -= 10;
|
|
issues.push('Hard to verify - add measurable outcomes (e.g., "all", "never", "100%")');
|
|
}
|
|
|
|
return {
|
|
score: Math.max(0, Math.min(100, score)),
|
|
issues,
|
|
strengths
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Suggest optimizations for a rule
|
|
*
|
|
* @param {string} ruleText - The rule text to optimize
|
|
* @returns {Array<Object>} Array of suggestions with before/after
|
|
*/
|
|
suggestOptimizations(ruleText) {
|
|
const suggestions = [];
|
|
|
|
// Suggest replacing weak language with strong imperatives
|
|
this.weakWords.forEach(word => {
|
|
const regex = new RegExp(`\\b${word}\\b`, 'i');
|
|
if (regex.test(ruleText)) {
|
|
let replacement = 'MUST';
|
|
if (word === 'try' || word === 'attempt') replacement = 'MUST';
|
|
else if (word === 'should') replacement = 'MUST';
|
|
else if (word === 'consider') replacement = 'MUST';
|
|
|
|
const optimized = ruleText.replace(
|
|
new RegExp(`\\b${word}\\b`, 'i'),
|
|
replacement
|
|
);
|
|
|
|
suggestions.push({
|
|
type: 'weak_language',
|
|
severity: 'high',
|
|
original: word,
|
|
before: ruleText,
|
|
after: optimized,
|
|
reason: `"${word}" is weak language - replaced with "${replacement}" for clarity`
|
|
});
|
|
}
|
|
});
|
|
|
|
// Suggest replacing vague terms with variables
|
|
if (ruleText.toLowerCase().includes('this project')) {
|
|
suggestions.push({
|
|
type: 'vague_term',
|
|
severity: 'medium',
|
|
original: 'this project',
|
|
before: ruleText,
|
|
after: ruleText.replace(/this project/gi, '${PROJECT_NAME} project'),
|
|
reason: '"this project" is vague - use ${PROJECT_NAME} variable for clarity'
|
|
});
|
|
}
|
|
|
|
if (ruleText.toLowerCase().includes('the database') && !/\$\{DB_TYPE\}/.test(ruleText)) {
|
|
suggestions.push({
|
|
type: 'vague_term',
|
|
severity: 'medium',
|
|
original: 'the database',
|
|
before: ruleText,
|
|
after: ruleText.replace(/the database/gi, '${DB_TYPE} database'),
|
|
reason: '"the database" is vague - specify ${DB_TYPE} for clarity'
|
|
});
|
|
}
|
|
|
|
// Suggest adding strong imperatives if missing
|
|
const hasStrong = this.strongWords.some(word =>
|
|
new RegExp(`\\b${word}\\b`).test(ruleText)
|
|
);
|
|
if (!hasStrong && !ruleText.startsWith('MUST') && !ruleText.startsWith('SHALL')) {
|
|
suggestions.push({
|
|
type: 'missing_imperative',
|
|
severity: 'high',
|
|
original: `${ruleText.substring(0, 20) }...`,
|
|
before: ruleText,
|
|
after: `MUST ${ ruleText.charAt(0).toLowerCase() }${ruleText.slice(1)}`,
|
|
reason: 'No strong imperative - added "MUST" at start for clarity'
|
|
});
|
|
}
|
|
|
|
// Suggest removing hedging
|
|
this.hedgingPhrases.forEach(phrase => {
|
|
if (ruleText.toLowerCase().includes(phrase)) {
|
|
suggestions.push({
|
|
type: 'hedging',
|
|
severity: 'medium',
|
|
original: phrase,
|
|
before: ruleText,
|
|
after: ruleText.replace(new RegExp(phrase, 'gi'), ''),
|
|
reason: `"${phrase}" is hedging language - removed for certainty`
|
|
});
|
|
}
|
|
});
|
|
|
|
return suggestions;
|
|
}
|
|
|
|
/**
|
|
* Auto-optimize a rule text
|
|
*
|
|
* @param {string} ruleText - The rule text to optimize
|
|
* @param {Object} options - Optimization options
|
|
* @param {string} options.mode - 'aggressive' (apply all) or 'conservative' (apply safe ones)
|
|
* @returns {Object} { optimized: string, changes: Array, before: string, after: string }
|
|
*/
|
|
optimize(ruleText, options = { mode: 'conservative' }) {
|
|
const suggestions = this.suggestOptimizations(ruleText);
|
|
let optimized = ruleText;
|
|
const appliedChanges = [];
|
|
|
|
suggestions.forEach(suggestion => {
|
|
// In conservative mode, only apply high-severity changes
|
|
if (options.mode === 'conservative' && suggestion.severity !== 'high') {
|
|
return;
|
|
}
|
|
|
|
// Apply the optimization
|
|
optimized = suggestion.after;
|
|
appliedChanges.push({
|
|
type: suggestion.type,
|
|
original: suggestion.original,
|
|
reason: suggestion.reason
|
|
});
|
|
});
|
|
|
|
return {
|
|
optimized,
|
|
changes: appliedChanges,
|
|
before: ruleText,
|
|
after: optimized,
|
|
improvementScore: this._calculateImprovement(ruleText, optimized)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate improvement score between before and after
|
|
*
|
|
* @private
|
|
* @param {string} before - Original rule text
|
|
* @param {string} after - Optimized rule text
|
|
* @returns {number} Improvement percentage
|
|
*/
|
|
_calculateImprovement(before, after) {
|
|
const scoreBefore = this.analyzeClarity(before).score;
|
|
const scoreAfter = this.analyzeClarity(after).score;
|
|
|
|
return Math.round(((scoreAfter - scoreBefore) / scoreBefore) * 100);
|
|
}
|
|
|
|
/**
|
|
* Comprehensive rule analysis
|
|
*
|
|
* Runs all analysis types and returns complete report
|
|
*
|
|
* @param {string} ruleText - The rule text to analyze
|
|
* @returns {Object} Complete analysis report
|
|
*/
|
|
analyzeRule(ruleText) {
|
|
const clarity = this.analyzeClarity(ruleText);
|
|
const specificity = this.analyzeSpecificity(ruleText);
|
|
const actionability = this.analyzeActionability(ruleText);
|
|
const suggestions = this.suggestOptimizations(ruleText);
|
|
|
|
// Calculate overall quality score (weighted average)
|
|
const overallScore = Math.round(
|
|
(clarity.score * 0.4) +
|
|
(specificity.score * 0.3) +
|
|
(actionability.score * 0.3)
|
|
);
|
|
|
|
return {
|
|
overallScore,
|
|
clarity: {
|
|
score: clarity.score,
|
|
grade: this._getGrade(clarity.score),
|
|
issues: clarity.issues,
|
|
strengths: clarity.strengths
|
|
},
|
|
specificity: {
|
|
score: specificity.score,
|
|
grade: this._getGrade(specificity.score),
|
|
issues: specificity.issues,
|
|
strengths: specificity.strengths
|
|
},
|
|
actionability: {
|
|
score: actionability.score,
|
|
grade: this._getGrade(actionability.score),
|
|
issues: actionability.issues,
|
|
strengths: actionability.strengths
|
|
},
|
|
suggestions,
|
|
recommendedAction: this._getRecommendedAction(overallScore, suggestions)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert score to letter grade
|
|
* @private
|
|
*/
|
|
_getGrade(score) {
|
|
if (score >= 90) return 'A';
|
|
if (score >= 80) return 'B';
|
|
if (score >= 70) return 'C';
|
|
if (score >= 60) return 'D';
|
|
return 'F';
|
|
}
|
|
|
|
/**
|
|
* Get recommended action based on score and suggestions
|
|
* @private
|
|
*/
|
|
_getRecommendedAction(score, suggestions) {
|
|
if (score >= 90) {
|
|
return 'Excellent - no changes needed';
|
|
}
|
|
if (score >= 80) {
|
|
return 'Good - consider minor improvements';
|
|
}
|
|
if (score >= 70) {
|
|
return 'Acceptable - improvements recommended';
|
|
}
|
|
if (score >= 60) {
|
|
return 'Needs improvement - apply suggestions';
|
|
}
|
|
return 'Poor quality - significant rewrite needed';
|
|
}
|
|
}
|
|
|
|
module.exports = new RuleOptimizer();
|