Major Features:
- Multi-project governance with Rule Manager web UI
- Project Manager for organizing governance across projects
- Variable substitution system (${VAR_NAME} in rules)
- Claude.md analyzer for instruction extraction
- Rule quality scoring and optimization
Admin UI Components:
- /admin/rule-manager.html - Full-featured rule management interface
- /admin/project-manager.html - Multi-project administration
- /admin/claude-md-migrator.html - Import rules from Claude.md files
- Dashboard enhancements for governance analytics
Backend Implementation:
- Controllers: projects, rules, variables
- Models: Project, VariableValue, enhanced GovernanceRule
- Routes: /api/projects, /api/rules with full CRUD
- Services: ClaudeMdAnalyzer, RuleOptimizer, VariableSubstitution
- Utilities: mongoose helpers
Documentation:
- User guides for Rule Manager and Projects
- Complete API documentation (PROJECTS_API, RULES_API)
- Phase 3 planning and architecture diagrams
- Test results and error analysis
- Coding best practices summary
Testing & Scripts:
- Integration tests for projects API
- Unit tests for variable substitution
- Database migration scripts
- Seed data generation
- Test token generator
Key Capabilities:
✅ UNIVERSAL scope rules apply across all projects
✅ PROJECT_SPECIFIC rules override for individual projects
✅ Variable substitution per-project (e.g., ${DB_PORT} → 27017)
✅ Real-time validation and quality scoring
✅ Advanced filtering and search
✅ Import from existing Claude.md files
Technical Details:
- MongoDB-backed governance persistence
- RESTful API with Express
- JWT authentication for admin endpoints
- CSP-compliant frontend (no inline handlers)
- Responsive Tailwind UI
This implements Phase 3 architecture as documented in planning docs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
840 lines
24 KiB
JavaScript
840 lines
24 KiB
JavaScript
/**
|
|
* Rules Controller
|
|
* Multi-Project Governance Manager - Rule Management API
|
|
*
|
|
* Provides CRUD operations and advanced querying for governance rules
|
|
*/
|
|
|
|
const GovernanceRule = require('../models/GovernanceRule.model');
|
|
const VariableSubstitutionService = require('../services/VariableSubstitution.service');
|
|
const logger = require('../utils/logger.util');
|
|
|
|
/**
|
|
* GET /api/admin/rules
|
|
* List all rules with filtering, sorting, and pagination
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.query - Query parameters
|
|
* @param {string} [req.query.scope] - Filter by scope (UNIVERSAL | PROJECT_SPECIFIC)
|
|
* @param {string} [req.query.quadrant] - Filter by quadrant (STRATEGIC | OPERATIONAL | TACTICAL | SYSTEM | STORAGE)
|
|
* @param {string} [req.query.persistence] - Filter by persistence (HIGH | MEDIUM | LOW)
|
|
* @param {string} [req.query.category] - Filter by category
|
|
* @param {boolean} [req.query.active] - Filter by active status
|
|
* @param {string} [req.query.validationStatus] - Filter by validation status
|
|
* @param {string} [req.query.projectId] - Filter by applicable project
|
|
* @param {string} [req.query.search] - Full-text search in rule text
|
|
* @param {string} [req.query.sort='priority'] - Sort field (priority | clarity | id | updatedAt)
|
|
* @param {string} [req.query.order='desc'] - Sort order (asc | desc)
|
|
* @param {number} [req.query.page=1] - Page number
|
|
* @param {number} [req.query.limit=20] - Items per page
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} JSON response with rules array and pagination metadata
|
|
*
|
|
* @example
|
|
* // Get all active SYSTEM rules, sorted by priority
|
|
* GET /api/admin/rules?quadrant=SYSTEM&active=true&sort=priority&order=desc
|
|
*/
|
|
async function listRules(req, res) {
|
|
try {
|
|
const {
|
|
// Filtering
|
|
scope, // UNIVERSAL | PROJECT_SPECIFIC
|
|
quadrant, // STRATEGIC | OPERATIONAL | TACTICAL | SYSTEM | STORAGE
|
|
persistence, // HIGH | MEDIUM | LOW
|
|
category, // content | security | privacy | technical | process | values | other
|
|
active, // true | false
|
|
validationStatus, // PASSED | FAILED | NEEDS_REVIEW | NOT_VALIDATED
|
|
projectId, // Filter by applicable project
|
|
search, // Text search in rule text
|
|
|
|
// Sorting
|
|
sort = 'priority', // priority | clarity | id | updatedAt
|
|
order = 'desc', // asc | desc
|
|
|
|
// Pagination
|
|
page = 1,
|
|
limit = 20
|
|
} = req.query;
|
|
|
|
// Build query
|
|
const query = {};
|
|
|
|
if (scope) query.scope = scope;
|
|
if (quadrant) query.quadrant = quadrant;
|
|
if (persistence) query.persistence = persistence;
|
|
if (category) query.category = category;
|
|
if (active !== undefined) query.active = active === 'true';
|
|
if (validationStatus) query.validationStatus = validationStatus;
|
|
|
|
// Project-specific filtering
|
|
if (projectId) {
|
|
query.$or = [
|
|
{ scope: 'UNIVERSAL' },
|
|
{ applicableProjects: projectId },
|
|
{ applicableProjects: '*' }
|
|
];
|
|
}
|
|
|
|
// Text search
|
|
if (search) {
|
|
query.$text = { $search: search };
|
|
}
|
|
|
|
// Build sort
|
|
const sortField = sort === 'clarity' ? 'clarityScore' : sort;
|
|
const sortOrder = order === 'asc' ? 1 : -1;
|
|
const sortQuery = { [sortField]: sortOrder };
|
|
|
|
// Execute query with pagination
|
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
|
|
|
let [rules, total] = await Promise.all([
|
|
GovernanceRule.find(query)
|
|
.sort(sortQuery)
|
|
.skip(skip)
|
|
.limit(parseInt(limit))
|
|
.lean(),
|
|
GovernanceRule.countDocuments(query)
|
|
]);
|
|
|
|
// If projectId provided, substitute variables to show context-aware text
|
|
if (projectId) {
|
|
rules = await VariableSubstitutionService.substituteRules(rules, projectId);
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
rules,
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
pages: Math.ceil(total / parseInt(limit))
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List rules error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to retrieve rules'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/admin/rules/stats
|
|
* Get dashboard statistics including counts by scope, quadrant, persistence,
|
|
* validation status, and average quality scores
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} JSON response with comprehensive statistics
|
|
*
|
|
* @example
|
|
* // Response format:
|
|
* {
|
|
* success: true,
|
|
* stats: {
|
|
* total: 18,
|
|
* byScope: { UNIVERSAL: 0, PROJECT_SPECIFIC: 18 },
|
|
* byQuadrant: [{ quadrant: 'SYSTEM', count: 7 }, ...],
|
|
* byPersistence: [{ persistence: 'HIGH', count: 17 }, ...],
|
|
* byValidationStatus: { NOT_VALIDATED: 18 },
|
|
* averageScores: { clarity: 85.5, specificity: null, actionability: null },
|
|
* totalChecks: 0,
|
|
* totalViolations: 0
|
|
* }
|
|
* }
|
|
*/
|
|
async function getRuleStats(req, res) {
|
|
try {
|
|
const stats = await GovernanceRule.getStatistics();
|
|
|
|
// Count rules by scope
|
|
const scopeCounts = await GovernanceRule.aggregate([
|
|
{ $match: { active: true } },
|
|
{
|
|
$group: {
|
|
_id: '$scope',
|
|
count: { $sum: 1 }
|
|
}
|
|
}
|
|
]);
|
|
|
|
// Count rules by validation status
|
|
const validationCounts = await GovernanceRule.aggregate([
|
|
{ $match: { active: true } },
|
|
{
|
|
$group: {
|
|
_id: '$validationStatus',
|
|
count: { $sum: 1 }
|
|
}
|
|
}
|
|
]);
|
|
|
|
// Format response
|
|
const response = {
|
|
success: true,
|
|
stats: {
|
|
total: stats?.totalRules || 0,
|
|
byScope: scopeCounts.reduce((acc, item) => {
|
|
acc[item._id] = item.count;
|
|
return acc;
|
|
}, {}),
|
|
byQuadrant: stats?.byQuadrant || [],
|
|
byPersistence: stats?.byPersistence || [],
|
|
byValidationStatus: validationCounts.reduce((acc, item) => {
|
|
acc[item._id] = item.count;
|
|
return acc;
|
|
}, {}),
|
|
averageScores: {
|
|
clarity: stats?.avgClarityScore || null,
|
|
specificity: stats?.avgSpecificityScore || null,
|
|
actionability: stats?.avgActionabilityScore || null
|
|
},
|
|
totalChecks: stats?.totalChecks || 0,
|
|
totalViolations: stats?.totalViolations || 0
|
|
}
|
|
};
|
|
|
|
res.json(response);
|
|
|
|
} catch (error) {
|
|
logger.error('Get rule stats error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to retrieve statistics'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/admin/rules/:id
|
|
* Get a single rule with full details including validation results,
|
|
* usage statistics, and optimization history
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.params - URL parameters
|
|
* @param {string} req.params.id - Rule ID (inst_xxx) or MongoDB ObjectId
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} JSON response with complete rule document
|
|
*
|
|
* @example
|
|
* GET /api/admin/rules/inst_001
|
|
* GET /api/admin/rules/68e8c3a6499d095048311f03
|
|
*/
|
|
async function getRule(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const rule = await GovernanceRule.findOne({
|
|
$or: [
|
|
{ id },
|
|
{ _id: mongoose.Types.ObjectId.isValid(id) ? id : null }
|
|
]
|
|
});
|
|
|
|
if (!rule) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Rule not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
rule
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get rule error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to retrieve rule'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/admin/rules
|
|
* Create a new governance rule with automatic variable detection and clarity scoring
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.body - Rule data
|
|
* @param {string} req.body.id - Unique rule ID (e.g., 'inst_019')
|
|
* @param {string} req.body.text - Rule text (may contain ${VARIABLE} placeholders)
|
|
* @param {string} [req.body.scope='PROJECT_SPECIFIC'] - UNIVERSAL | PROJECT_SPECIFIC
|
|
* @param {Array<string>} [req.body.applicableProjects=['*']] - Project IDs or ['*'] for all
|
|
* @param {string} req.body.quadrant - STRATEGIC | OPERATIONAL | TACTICAL | SYSTEM | STORAGE
|
|
* @param {string} req.body.persistence - HIGH | MEDIUM | LOW
|
|
* @param {string} [req.body.category='other'] - Rule category
|
|
* @param {number} [req.body.priority=50] - Priority (0-100)
|
|
* @param {string} [req.body.temporalScope='PERMANENT'] - IMMEDIATE | SESSION | PROJECT | PERMANENT
|
|
* @param {boolean} [req.body.active=true] - Whether rule is active
|
|
* @param {Array<string>} [req.body.examples=[]] - Example scenarios
|
|
* @param {Array<string>} [req.body.relatedRules=[]] - IDs of related rules
|
|
* @param {string} [req.body.notes=''] - Additional notes
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} JSON response with created rule
|
|
*
|
|
* @example
|
|
* POST /api/admin/rules
|
|
* {
|
|
* "id": "inst_019",
|
|
* "text": "Database MUST use ${DB_TYPE} on port ${DB_PORT}",
|
|
* "scope": "UNIVERSAL",
|
|
* "quadrant": "SYSTEM",
|
|
* "persistence": "HIGH",
|
|
* "priority": 90
|
|
* }
|
|
*/
|
|
async function createRule(req, res) {
|
|
try {
|
|
const {
|
|
id,
|
|
text,
|
|
scope = 'PROJECT_SPECIFIC',
|
|
applicableProjects = ['*'],
|
|
quadrant,
|
|
persistence,
|
|
category = 'other',
|
|
priority = 50,
|
|
temporalScope = 'PERMANENT',
|
|
active = true,
|
|
examples = [],
|
|
relatedRules = [],
|
|
notes = ''
|
|
} = req.body;
|
|
|
|
// Validation
|
|
if (!id || !text || !quadrant || !persistence) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required fields: id, text, quadrant, persistence'
|
|
});
|
|
}
|
|
|
|
// Check if rule ID already exists
|
|
const existing = await GovernanceRule.findOne({ id });
|
|
if (existing) {
|
|
return res.status(409).json({
|
|
error: 'Conflict',
|
|
message: `Rule with ID ${id} already exists`
|
|
});
|
|
}
|
|
|
|
// Auto-detect variables in text
|
|
const variables = [];
|
|
const varPattern = /\$\{([A-Z_]+)\}/g;
|
|
let match;
|
|
while ((match = varPattern.exec(text)) !== null) {
|
|
if (!variables.includes(match[1])) {
|
|
variables.push(match[1]);
|
|
}
|
|
}
|
|
|
|
// Calculate basic clarity score (heuristic - will be improved by AI optimizer)
|
|
let clarityScore = 100;
|
|
const weakWords = ['try', 'maybe', 'consider', 'might', 'probably', 'possibly'];
|
|
weakWords.forEach(word => {
|
|
if (new RegExp(`\\b${word}\\b`, 'i').test(text)) {
|
|
clarityScore -= 10;
|
|
}
|
|
});
|
|
const strongWords = ['MUST', 'SHALL', 'REQUIRED', 'PROHIBITED'];
|
|
const hasStrong = strongWords.some(word => new RegExp(`\\b${word}\\b`).test(text));
|
|
if (!hasStrong) clarityScore -= 10;
|
|
clarityScore = Math.max(0, Math.min(100, clarityScore));
|
|
|
|
// Create rule
|
|
const rule = new GovernanceRule({
|
|
id,
|
|
text,
|
|
scope,
|
|
applicableProjects,
|
|
variables,
|
|
quadrant,
|
|
persistence,
|
|
category,
|
|
priority,
|
|
temporalScope,
|
|
active,
|
|
examples,
|
|
relatedRules,
|
|
notes,
|
|
source: 'user_instruction',
|
|
createdBy: req.user?.email || 'admin',
|
|
clarityScore,
|
|
validationStatus: 'NOT_VALIDATED',
|
|
usageStats: {
|
|
referencedInProjects: [],
|
|
timesEnforced: 0,
|
|
conflictsDetected: 0,
|
|
lastEnforced: null
|
|
}
|
|
});
|
|
|
|
await rule.save();
|
|
|
|
logger.info(`Rule created: ${id} by ${req.user?.email}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
rule,
|
|
message: 'Rule created successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Create rule error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to create rule'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/admin/rules/:id
|
|
* Update an existing rule with automatic variable re-detection, clarity re-scoring,
|
|
* and optimization history tracking
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.params - URL parameters
|
|
* @param {string} req.params.id - Rule ID (inst_xxx) or MongoDB ObjectId
|
|
* @param {Object} req.body - Fields to update (partial update supported)
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} JSON response with updated rule
|
|
*
|
|
* @description
|
|
* - Automatically re-detects variables if text changes
|
|
* - Recalculates clarity score if text changes
|
|
* - Adds entry to optimization history if text changes
|
|
* - Resets validation status to NOT_VALIDATED if text changes
|
|
*
|
|
* @example
|
|
* PUT /api/admin/rules/inst_001
|
|
* {
|
|
* "text": "MongoDB MUST run on port 27017 for ${PROJECT_NAME} database",
|
|
* "priority": 95
|
|
* }
|
|
*/
|
|
async function updateRule(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates = req.body;
|
|
|
|
// Find rule
|
|
const rule = await GovernanceRule.findOne({
|
|
$or: [
|
|
{ id },
|
|
{ _id: mongoose.Types.ObjectId.isValid(id) ? id : null }
|
|
]
|
|
});
|
|
|
|
if (!rule) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Rule not found'
|
|
});
|
|
}
|
|
|
|
// Track changes for optimization history
|
|
const before = rule.text;
|
|
|
|
// Update fields (whitelist approach for security)
|
|
const allowedFields = [
|
|
'text', 'scope', 'applicableProjects', 'variables',
|
|
'quadrant', 'persistence', 'category', 'priority',
|
|
'temporalScope', 'active', 'examples', 'relatedRules', 'notes'
|
|
];
|
|
|
|
allowedFields.forEach(field => {
|
|
if (updates[field] !== undefined) {
|
|
rule[field] = updates[field];
|
|
}
|
|
});
|
|
|
|
// If text changed, re-detect variables and update clarity score
|
|
if (updates.text && updates.text !== before) {
|
|
const variables = [];
|
|
const varPattern = /\$\{([A-Z_]+)\}/g;
|
|
let match;
|
|
while ((match = varPattern.exec(updates.text)) !== null) {
|
|
if (!variables.includes(match[1])) {
|
|
variables.push(match[1]);
|
|
}
|
|
}
|
|
rule.variables = variables;
|
|
|
|
// Recalculate basic clarity score
|
|
let clarityScore = 100;
|
|
const weakWords = ['try', 'maybe', 'consider', 'might', 'probably', 'possibly'];
|
|
weakWords.forEach(word => {
|
|
if (new RegExp(`\\b${word}\\b`, 'i').test(updates.text)) {
|
|
clarityScore -= 10;
|
|
}
|
|
});
|
|
const strongWords = ['MUST', 'SHALL', 'REQUIRED', 'PROHIBITED'];
|
|
const hasStrong = strongWords.some(word => new RegExp(`\\b${word}\\b`).test(updates.text));
|
|
if (!hasStrong) clarityScore -= 10;
|
|
rule.clarityScore = Math.max(0, Math.min(100, clarityScore));
|
|
|
|
// Add to optimization history if text changed significantly
|
|
if (rule.optimizationHistory && before !== updates.text) {
|
|
rule.optimizationHistory.push({
|
|
timestamp: new Date(),
|
|
before,
|
|
after: updates.text,
|
|
reason: 'Manual edit by user',
|
|
scores: {
|
|
clarity: rule.clarityScore,
|
|
specificity: rule.specificityScore,
|
|
actionability: rule.actionabilityScore
|
|
}
|
|
});
|
|
}
|
|
|
|
// Reset validation status (needs re-validation after change)
|
|
rule.validationStatus = 'NOT_VALIDATED';
|
|
rule.lastValidated = null;
|
|
}
|
|
|
|
await rule.save();
|
|
|
|
logger.info(`Rule updated: ${rule.id} by ${req.user?.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
rule,
|
|
message: 'Rule updated successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Update rule error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to update rule'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/admin/rules/:id
|
|
* Soft delete (deactivate) or permanently delete a rule
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.params - URL parameters
|
|
* @param {string} req.params.id - Rule ID (inst_xxx) or MongoDB ObjectId
|
|
* @param {Object} req.query - Query parameters
|
|
* @param {boolean} [req.query.permanent=false] - If 'true', hard delete; otherwise soft delete
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} JSON response confirming deletion
|
|
*
|
|
* @description
|
|
* - Default behavior: Soft delete (sets active=false, preserves data)
|
|
* - Prevents deletion of UNIVERSAL rules that are in use by projects
|
|
* - Use permanent=true query param for hard delete (use with caution)
|
|
*
|
|
* @example
|
|
* // Soft delete (recommended)
|
|
* DELETE /api/admin/rules/inst_001
|
|
*
|
|
* // Permanent delete (use with caution)
|
|
* DELETE /api/admin/rules/inst_001?permanent=true
|
|
*/
|
|
async function deleteRule(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { permanent = false } = req.query;
|
|
|
|
const rule = await GovernanceRule.findOne({
|
|
$or: [
|
|
{ id },
|
|
{ _id: mongoose.Types.ObjectId.isValid(id) ? id : null }
|
|
]
|
|
});
|
|
|
|
if (!rule) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Rule not found'
|
|
});
|
|
}
|
|
|
|
// Check if rule is in use
|
|
if (rule.scope === 'UNIVERSAL' && rule.usageStats?.referencedInProjects?.length > 0) {
|
|
return res.status(409).json({
|
|
error: 'Conflict',
|
|
message: `Rule is used by ${rule.usageStats.referencedInProjects.length} projects. Cannot delete.`,
|
|
projects: rule.usageStats.referencedInProjects
|
|
});
|
|
}
|
|
|
|
if (permanent === 'true') {
|
|
// Hard delete (use with caution)
|
|
await rule.deleteOne();
|
|
logger.warn(`Rule permanently deleted: ${rule.id} by ${req.user?.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Rule permanently deleted'
|
|
});
|
|
} else {
|
|
// Soft delete
|
|
rule.active = false;
|
|
await rule.save();
|
|
logger.info(`Rule deactivated: ${rule.id} by ${req.user?.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
rule,
|
|
message: 'Rule deactivated successfully'
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('Delete rule error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to delete rule'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/admin/rules/:id/optimize
|
|
* Optimize a single rule using AI analysis
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.params - URL parameters
|
|
* @param {string} req.params.id - Rule ID
|
|
* @param {Object} req.body - Optimization options
|
|
* @param {string} [req.body.mode='conservative'] - 'aggressive' or 'conservative'
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} Optimization analysis and suggestions
|
|
*/
|
|
async function optimizeRule(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { mode = 'conservative' } = req.body;
|
|
|
|
// Find rule
|
|
const rule = await GovernanceRule.findOne({
|
|
$or: [
|
|
{ id },
|
|
{ _id: mongoose.Types.ObjectId.isValid(id) ? id : null }
|
|
]
|
|
});
|
|
|
|
if (!rule) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Rule not found'
|
|
});
|
|
}
|
|
|
|
const RuleOptimizer = require('../services/RuleOptimizer.service');
|
|
|
|
// Perform comprehensive analysis
|
|
const analysis = RuleOptimizer.analyzeRule(rule.text);
|
|
|
|
// Get auto-optimization result
|
|
const optimized = RuleOptimizer.optimize(rule.text, { mode });
|
|
|
|
res.json({
|
|
success: true,
|
|
rule: {
|
|
id: rule.id,
|
|
originalText: rule.text
|
|
},
|
|
analysis,
|
|
optimization: {
|
|
optimizedText: optimized.optimized,
|
|
changes: optimized.changes,
|
|
improvementScore: optimized.improvementScore,
|
|
mode
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Optimize rule error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to optimize rule'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/admin/rules/analyze-claude-md
|
|
* Analyze CLAUDE.md content and extract candidate rules
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.body - Request body
|
|
* @param {string} req.body.content - CLAUDE.md file content
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} Complete analysis with candidates, redundancies, and migration plan
|
|
*/
|
|
async function analyzeClaudeMd(req, res) {
|
|
try {
|
|
const { content } = req.body;
|
|
|
|
if (!content || content.trim().length === 0) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'CLAUDE.md content is required'
|
|
});
|
|
}
|
|
|
|
const ClaudeMdAnalyzer = require('../services/ClaudeMdAnalyzer.service');
|
|
|
|
// Perform complete analysis
|
|
const analysis = ClaudeMdAnalyzer.analyze(content);
|
|
|
|
res.json({
|
|
success: true,
|
|
analysis: {
|
|
totalStatements: analysis.candidates.length,
|
|
quality: analysis.quality,
|
|
candidates: analysis.candidates,
|
|
redundancies: analysis.redundancies,
|
|
migrationPlan: analysis.migrationPlan
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Analyze CLAUDE.md error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to analyze CLAUDE.md'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/admin/rules/migrate-from-claude-md
|
|
* Create rules from selected CLAUDE.md candidates
|
|
*
|
|
* @param {Object} req - Express request object
|
|
* @param {Object} req.body - Request body
|
|
* @param {Array<Object>} req.body.selectedCandidates - Array of candidate rules to create
|
|
* @param {Object} res - Express response object
|
|
*
|
|
* @returns {Object} Results of rule creation (created, failed)
|
|
*/
|
|
async function migrateFromClaudeMd(req, res) {
|
|
try {
|
|
const { selectedCandidates } = req.body;
|
|
|
|
if (!Array.isArray(selectedCandidates) || selectedCandidates.length === 0) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'selectedCandidates array is required'
|
|
});
|
|
}
|
|
|
|
const results = {
|
|
created: [],
|
|
failed: [],
|
|
totalRequested: selectedCandidates.length
|
|
};
|
|
|
|
// Generate unique IDs for new rules
|
|
const existingRules = await GovernanceRule.find({}, 'id').lean();
|
|
const existingIds = existingRules.map(r => r.id);
|
|
let nextId = 1;
|
|
|
|
// Find next available ID
|
|
while (existingIds.includes(`inst_${String(nextId).padStart(3, '0')}`)) {
|
|
nextId++;
|
|
}
|
|
|
|
// Create rules from candidates
|
|
for (const candidate of selectedCandidates) {
|
|
try {
|
|
const ruleId = `inst_${String(nextId).padStart(3, '0')}`;
|
|
nextId++;
|
|
|
|
const rule = new GovernanceRule({
|
|
id: ruleId,
|
|
text: candidate.suggestedRule.text,
|
|
scope: candidate.suggestedRule.scope,
|
|
applicableProjects: candidate.suggestedRule.scope === 'UNIVERSAL' ? ['*'] : [],
|
|
variables: candidate.suggestedRule.variables || [],
|
|
quadrant: candidate.quadrant,
|
|
persistence: candidate.persistence,
|
|
category: 'other',
|
|
priority: candidate.persistence === 'HIGH' ? 90 : (candidate.persistence === 'MEDIUM' ? 70 : 50),
|
|
temporalScope: 'PERMANENT',
|
|
active: true,
|
|
examples: [],
|
|
relatedRules: [],
|
|
notes: `Migrated from CLAUDE.md. Original: ${candidate.originalText}`,
|
|
source: 'claude_md_migration',
|
|
createdBy: req.user?.email || 'admin',
|
|
clarityScore: candidate.suggestedRule.clarityScore,
|
|
validationStatus: 'NOT_VALIDATED',
|
|
usageStats: {
|
|
referencedInProjects: [],
|
|
timesEnforced: 0,
|
|
conflictsDetected: 0,
|
|
lastEnforced: null
|
|
}
|
|
});
|
|
|
|
await rule.save();
|
|
results.created.push({
|
|
id: ruleId,
|
|
text: rule.text,
|
|
original: candidate.originalText
|
|
});
|
|
|
|
logger.info(`Rule migrated from CLAUDE.md: ${ruleId}`);
|
|
|
|
} catch (error) {
|
|
results.failed.push({
|
|
candidate: candidate.originalText,
|
|
error: error.message
|
|
});
|
|
logger.error(`Failed to migrate candidate:`, error);
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
results,
|
|
message: `Created ${results.created.length} of ${results.totalRequested} rules`
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Migrate from CLAUDE.md error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to migrate rules from CLAUDE.md'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Import mongoose for ObjectId validation
|
|
const mongoose = require('mongoose');
|
|
|
|
module.exports = {
|
|
listRules,
|
|
getRuleStats,
|
|
getRule,
|
|
createRule,
|
|
updateRule,
|
|
deleteRule,
|
|
optimizeRule,
|
|
analyzeClaudeMd,
|
|
migrateFromClaudeMd
|
|
};
|