feat(bi): add business intelligence backend infrastructure
Implements core BI analytics capabilities for governance ROI measurement: - Activity classifier utility for automatic event categorization * Detects activity type (client communication, infrastructure, etc.) * Calculates risk level, stakeholder impact, data sensitivity * Computes business impact scores (0-100) - Enhanced audit controller with BI analytics endpoints * Cost avoidance calculator with user-configurable factors * Framework maturity scoring (0-100 scale) * Team performance comparison (AI vs human) * Activity type breakdown and ROI projections - New API routes for cost configuration (GET/POST /api/admin/cost-config) - Hook validator enhancement * Automatic activity classification on governance decisions * MongoDB audit logging with BI context fields * Business impact scoring for blocked actions Status: Research prototype v1.0 Note: Cost factors are illustrative placeholders requiring validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bbed39623c
commit
ae12888eb4
4 changed files with 704 additions and 20 deletions
|
|
@ -274,7 +274,7 @@ function checkCredentialProtection() {
|
|||
}
|
||||
|
||||
const { validate } = require(validatorPath);
|
||||
const result = validate(FILE_PATH, 'edit');
|
||||
const result = validate(FILE_PATH, 'edit', HOOK_INPUT);
|
||||
|
||||
if (!result.valid) {
|
||||
return {
|
||||
|
|
@ -313,10 +313,11 @@ function updateSessionState() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Log metrics for hook execution
|
||||
* Log metrics for hook execution (both file-based and MongoDB)
|
||||
*/
|
||||
function logMetrics(result, reason = null) {
|
||||
async function logMetrics(result, reason = null) {
|
||||
try {
|
||||
// 1. File-based metrics (legacy)
|
||||
const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
|
||||
const METRICS_DIR = path.dirname(METRICS_PATH);
|
||||
|
||||
|
|
@ -367,11 +368,94 @@ function logMetrics(result, reason = null) {
|
|||
|
||||
// Write metrics
|
||||
fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
|
||||
|
||||
// 2. MongoDB audit log (for dashboard visibility)
|
||||
await logToAuditDatabase(result, reason);
|
||||
|
||||
} catch (err) {
|
||||
// Non-critical - don't fail on metrics logging
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log hook execution to MongoDB AuditLog for dashboard visibility
|
||||
*/
|
||||
async function logToAuditDatabase(result, reason) {
|
||||
try {
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Skip if not connected (hook runs before DB init)
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AuditLog = mongoose.model('AuditLog');
|
||||
|
||||
// Get session ID from state file
|
||||
let sessionId = 'unknown';
|
||||
try {
|
||||
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
||||
sessionId = sessionState.session_id || 'unknown';
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Determine rule violations based on reason
|
||||
const violations = [];
|
||||
if (result === 'blocked' && reason) {
|
||||
// Parse reason to extract rule ID if present
|
||||
const ruleMatch = reason.match(/inst_\d+/);
|
||||
violations.push({
|
||||
ruleId: ruleMatch ? ruleMatch[0] : 'hook-validator',
|
||||
ruleText: reason,
|
||||
severity: 'HIGH',
|
||||
details: reason
|
||||
});
|
||||
}
|
||||
|
||||
// Classify activity for business intelligence
|
||||
const activityClassifier = require('../../src/utils/activity-classifier.util');
|
||||
const classification = activityClassifier.classifyActivity('file_edit_hook', {
|
||||
filePath: FILE_PATH,
|
||||
reason: reason,
|
||||
service: 'FileEditHook'
|
||||
});
|
||||
|
||||
const businessImpact = activityClassifier.calculateBusinessImpact(
|
||||
classification,
|
||||
result === 'blocked'
|
||||
);
|
||||
|
||||
// Create audit log entry
|
||||
await AuditLog.create({
|
||||
sessionId: sessionId,
|
||||
action: 'file_edit_hook',
|
||||
allowed: result !== 'blocked',
|
||||
rulesChecked: ['inst_072', 'inst_084', 'inst_038'], // Common hook rules
|
||||
violations: violations,
|
||||
metadata: {
|
||||
filePath: FILE_PATH,
|
||||
hook: 'validate-file-edit',
|
||||
reason: reason
|
||||
},
|
||||
domain: 'SYSTEM',
|
||||
service: 'FileEditHook',
|
||||
timestamp: new Date(),
|
||||
// Business intelligence context
|
||||
activityType: classification.activityType,
|
||||
riskLevel: classification.riskLevel,
|
||||
stakeholderImpact: classification.stakeholderImpact,
|
||||
dataSensitivity: classification.dataSensitivity,
|
||||
reversibility: classification.reversibility,
|
||||
businessImpact: businessImpact
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
// Non-critical - don't fail on audit logging
|
||||
// console.error('Audit log error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read hook input from stdin
|
||||
*/
|
||||
|
|
@ -400,7 +484,7 @@ async function main() {
|
|||
|
||||
if (!HOOK_INPUT || !HOOK_INPUT.tool_input || !HOOK_INPUT.tool_input.file_path) {
|
||||
error('No file path provided in hook input');
|
||||
logMetrics('error', 'No file path in input');
|
||||
await logMetrics('error', 'No file path in input');
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
|
||||
|
|
@ -414,7 +498,7 @@ async function main() {
|
|||
if (cspCheck.output) {
|
||||
console.log(cspCheck.output);
|
||||
}
|
||||
logMetrics('blocked', cspCheck.reason);
|
||||
await logMetrics('blocked', cspCheck.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('CSP compliance validated on content after edit');
|
||||
|
|
@ -426,7 +510,7 @@ async function main() {
|
|||
if (preActionCheck.output) {
|
||||
console.log(preActionCheck.output);
|
||||
}
|
||||
logMetrics('blocked', preActionCheck.reason);
|
||||
await logMetrics('blocked', preActionCheck.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('Pre-action-check recency (inst_038) passed');
|
||||
|
|
@ -438,7 +522,7 @@ async function main() {
|
|||
conflicts.conflicts.forEach(c => {
|
||||
log(` • ${c.id}: ${c.instruction} [${c.quadrant}]`, 'yellow');
|
||||
});
|
||||
logMetrics('blocked', conflicts.reason);
|
||||
await logMetrics('blocked', conflicts.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No instruction conflicts detected');
|
||||
|
|
@ -447,7 +531,7 @@ async function main() {
|
|||
const boundary = checkBoundaryViolation();
|
||||
if (!boundary.passed) {
|
||||
error(boundary.reason);
|
||||
logMetrics('blocked', boundary.reason);
|
||||
await logMetrics('blocked', boundary.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No boundary violations detected');
|
||||
|
|
@ -459,7 +543,7 @@ async function main() {
|
|||
if (credentials.output) {
|
||||
console.log(credentials.output);
|
||||
}
|
||||
logMetrics('blocked', credentials.reason);
|
||||
await logMetrics('blocked', credentials.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No protected credential changes detected');
|
||||
|
|
@ -470,7 +554,7 @@ async function main() {
|
|||
if (githubUrlCheck.output) {
|
||||
console.error(githubUrlCheck.output);
|
||||
}
|
||||
logMetrics('blocked', githubUrlCheck.reason);
|
||||
await logMetrics('blocked', githubUrlCheck.reason);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
}
|
||||
success('No unauthorized GitHub URL modifications detected (inst_084)');
|
||||
|
|
@ -480,15 +564,15 @@ async function main() {
|
|||
updateSessionState();
|
||||
|
||||
// Log successful execution
|
||||
logMetrics('passed');
|
||||
await logMetrics('passed');
|
||||
|
||||
success('File edit validation complete\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
main().catch(async (err) => {
|
||||
error(`Hook validation error: ${err.message}`);
|
||||
logMetrics('error', err.message);
|
||||
await logMetrics('error', err.message);
|
||||
process.exit(2); // Exit code 2 = BLOCK
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ const fs = require('fs').promises;
|
|||
const path = require('path');
|
||||
const logger = require('../utils/logger.util');
|
||||
|
||||
// Default cost factors (user-configurable)
|
||||
const DEFAULT_COST_FACTORS = {
|
||||
CRITICAL: { amount: 50000, currency: 'USD', rationale: 'Average cost of critical security incident' },
|
||||
HIGH: { amount: 10000, currency: 'USD', rationale: 'Average cost of high-impact client-facing error' },
|
||||
MEDIUM: { amount: 2000, currency: 'USD', rationale: 'Average cost of medium-impact issue requiring hotfix' },
|
||||
LOW: { amount: 500, currency: 'USD', rationale: 'Average developer time cost to fix low-impact issue' }
|
||||
};
|
||||
|
||||
let userCostFactors = { ...DEFAULT_COST_FACTORS };
|
||||
|
||||
/**
|
||||
* Get audit logs for analytics
|
||||
* GET /api/admin/audit-logs
|
||||
|
|
@ -79,12 +89,24 @@ async function getAuditAnalytics(req, res) {
|
|||
const decisions = auditLogsResponse.decisions;
|
||||
|
||||
// Calculate analytics
|
||||
const blockedDecisions = decisions.filter(d => !d.allowed);
|
||||
const allowedDecisions = decisions.filter(d => d.allowed);
|
||||
|
||||
const analytics = {
|
||||
total: decisions.length,
|
||||
allowed: decisions.filter(d => d.allowed).length,
|
||||
blocked: decisions.filter(d => !d.allowed).length,
|
||||
allowed: allowedDecisions.length,
|
||||
blocked: blockedDecisions.length,
|
||||
violations: decisions.filter(d => d.violations && d.violations.length > 0).length,
|
||||
|
||||
// Block metrics
|
||||
blockRate: decisions.length > 0 ? ((blockedDecisions.length / decisions.length) * 100).toFixed(1) : 0,
|
||||
allowedRate: decisions.length > 0 ? ((allowedDecisions.length / decisions.length) * 100).toFixed(1) : 0,
|
||||
|
||||
// Block breakdown
|
||||
blocksByReason: {},
|
||||
blocksBySeverity: {},
|
||||
frameworkSaves: [], // High severity blocks
|
||||
|
||||
byAction: {},
|
||||
byService: {},
|
||||
bySession: {},
|
||||
|
|
@ -93,6 +115,14 @@ async function getAuditAnalytics(req, res) {
|
|||
timeline: [],
|
||||
topViolations: [],
|
||||
|
||||
// Business intelligence metrics
|
||||
byActivityType: {},
|
||||
byRiskLevel: {},
|
||||
byStakeholderImpact: {},
|
||||
riskHeatmap: [],
|
||||
businessImpactTotal: 0,
|
||||
roiProjections: {},
|
||||
|
||||
dateRange: auditLogsResponse.dateRange
|
||||
};
|
||||
|
||||
|
|
@ -134,21 +164,279 @@ async function getAuditAnalytics(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
// Top violations
|
||||
// Analyze blocks by reason and severity
|
||||
blockedDecisions.forEach(d => {
|
||||
// Extract reason from violations or metadata
|
||||
let reason = 'Unknown';
|
||||
let severity = 'MEDIUM';
|
||||
|
||||
if (d.violations && d.violations.length > 0) {
|
||||
// Use first violation's rule ID as reason
|
||||
reason = d.violations[0].ruleId || d.violations[0].ruleText || 'Unknown';
|
||||
severity = d.violations[0].severity || 'MEDIUM';
|
||||
|
||||
// Count by severity
|
||||
analytics.blocksBySeverity[severity] = (analytics.blocksBySeverity[severity] || 0) + 1;
|
||||
|
||||
// Track high severity blocks as "Framework Saves"
|
||||
if (severity === 'HIGH' || severity === 'CRITICAL') {
|
||||
analytics.frameworkSaves.push({
|
||||
timestamp: d.timestamp,
|
||||
reason: d.violations[0].ruleText || d.violations[0].details,
|
||||
ruleId: d.violations[0].ruleId,
|
||||
severity: severity,
|
||||
file: d.metadata?.filePath || d.metadata?.file || 'N/A',
|
||||
service: d.service
|
||||
});
|
||||
}
|
||||
} else if (d.metadata?.reason) {
|
||||
reason = d.metadata.reason;
|
||||
}
|
||||
|
||||
// Count by reason
|
||||
analytics.blocksByReason[reason] = (analytics.blocksByReason[reason] || 0) + 1;
|
||||
});
|
||||
|
||||
// Sort framework saves by timestamp (most recent first)
|
||||
analytics.frameworkSaves.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
analytics.frameworkSaves = analytics.frameworkSaves.slice(0, 10); // Top 10 saves
|
||||
|
||||
// Top violations (enhanced with more detail)
|
||||
const violationCounts = {};
|
||||
decisions.forEach(d => {
|
||||
if (d.violations && d.violations.length > 0) {
|
||||
d.violations.forEach(v => {
|
||||
violationCounts[v] = (violationCounts[v] || 0) + 1;
|
||||
const key = v.ruleId || v.ruleText || 'unknown';
|
||||
if (!violationCounts[key]) {
|
||||
violationCounts[key] = {
|
||||
ruleId: v.ruleId,
|
||||
ruleText: v.ruleText,
|
||||
severity: v.severity,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
violationCounts[key].count++;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
analytics.topViolations = Object.entries(violationCounts)
|
||||
.map(([violation, count]) => ({ violation, count }))
|
||||
analytics.topViolations = Object.values(violationCounts)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10);
|
||||
|
||||
// === BUSINESS INTELLIGENCE METRICS ===
|
||||
|
||||
// Activity type breakdown
|
||||
decisions.forEach(d => {
|
||||
const activityType = d.activityType || 'Unknown';
|
||||
if (!analytics.byActivityType[activityType]) {
|
||||
analytics.byActivityType[activityType] = {
|
||||
total: 0,
|
||||
allowed: 0,
|
||||
blocked: 0,
|
||||
blockRate: 0
|
||||
};
|
||||
}
|
||||
analytics.byActivityType[activityType].total++;
|
||||
if (d.allowed) {
|
||||
analytics.byActivityType[activityType].allowed++;
|
||||
} else {
|
||||
analytics.byActivityType[activityType].blocked++;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate block rates for each activity type
|
||||
Object.keys(analytics.byActivityType).forEach(type => {
|
||||
const data = analytics.byActivityType[type];
|
||||
data.blockRate = data.total > 0 ? ((data.blocked / data.total) * 100).toFixed(1) : '0.0';
|
||||
});
|
||||
|
||||
// Risk level distribution
|
||||
decisions.forEach(d => {
|
||||
const riskLevel = d.riskLevel || 'low';
|
||||
if (!analytics.byRiskLevel[riskLevel]) {
|
||||
analytics.byRiskLevel[riskLevel] = {
|
||||
total: 0,
|
||||
blocked: 0
|
||||
};
|
||||
}
|
||||
analytics.byRiskLevel[riskLevel].total++;
|
||||
if (!d.allowed) {
|
||||
analytics.byRiskLevel[riskLevel].blocked++;
|
||||
}
|
||||
});
|
||||
|
||||
// Stakeholder impact distribution
|
||||
decisions.forEach(d => {
|
||||
const impact = d.stakeholderImpact || 'individual';
|
||||
if (!analytics.byStakeholderImpact[impact]) {
|
||||
analytics.byStakeholderImpact[impact] = {
|
||||
total: 0,
|
||||
blocked: 0,
|
||||
businessImpact: 0
|
||||
};
|
||||
}
|
||||
analytics.byStakeholderImpact[impact].total++;
|
||||
if (!d.allowed) {
|
||||
analytics.byStakeholderImpact[impact].blocked++;
|
||||
analytics.byStakeholderImpact[impact].businessImpact += (d.businessImpact || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Risk heatmap: Activity Type vs Risk Level
|
||||
const heatmapData = {};
|
||||
decisions.forEach(d => {
|
||||
const activityType = d.activityType || 'Unknown';
|
||||
const riskLevel = d.riskLevel || 'low';
|
||||
const key = `${activityType}|${riskLevel}`;
|
||||
|
||||
if (!heatmapData[key]) {
|
||||
heatmapData[key] = {
|
||||
activityType,
|
||||
riskLevel,
|
||||
total: 0,
|
||||
blocked: 0
|
||||
};
|
||||
}
|
||||
heatmapData[key].total++;
|
||||
if (!d.allowed) {
|
||||
heatmapData[key].blocked++;
|
||||
}
|
||||
});
|
||||
analytics.riskHeatmap = Object.values(heatmapData);
|
||||
|
||||
// Business impact total
|
||||
analytics.businessImpactTotal = blockedDecisions.reduce((sum, d) => sum + (d.businessImpact || 0), 0);
|
||||
|
||||
// === COST AVOIDANCE CALCULATIONS ===
|
||||
|
||||
// Calculate cost avoided based on user-configured factors
|
||||
let totalCostAvoided = 0;
|
||||
const costBreakdown = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
||||
|
||||
blockedDecisions.forEach(d => {
|
||||
if (d.violations && d.violations.length > 0) {
|
||||
d.violations.forEach(v => {
|
||||
const severity = v.severity || 'LOW';
|
||||
const costFactor = userCostFactors[severity];
|
||||
if (costFactor) {
|
||||
totalCostAvoided += costFactor.amount;
|
||||
costBreakdown[severity] += costFactor.amount;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
analytics.costAvoidance = {
|
||||
total: totalCostAvoided,
|
||||
currency: userCostFactors.CRITICAL.currency,
|
||||
breakdown: costBreakdown,
|
||||
costFactors: userCostFactors
|
||||
};
|
||||
|
||||
// === AI vs HUMAN PERFORMANCE ===
|
||||
|
||||
// Detect AI vs Human based on service patterns
|
||||
// AI = FileEditHook, Framework services
|
||||
// Human = Manual overrides, direct database operations
|
||||
const aiDecisions = decisions.filter(d =>
|
||||
d.service === 'FileEditHook' ||
|
||||
d.service === 'BoundaryEnforcer' ||
|
||||
d.service === 'ContextPressureMonitor' ||
|
||||
d.service === 'MetacognitiveVerifier'
|
||||
);
|
||||
const humanDecisions = decisions.filter(d =>
|
||||
!aiDecisions.includes(d)
|
||||
);
|
||||
|
||||
const aiBlocked = aiDecisions.filter(d => !d.allowed).length;
|
||||
const humanBlocked = humanDecisions.filter(d => !d.allowed).length;
|
||||
|
||||
analytics.teamComparison = {
|
||||
ai: {
|
||||
total: aiDecisions.length,
|
||||
blocked: aiBlocked,
|
||||
blockRate: aiDecisions.length > 0 ? ((aiBlocked / aiDecisions.length) * 100).toFixed(1) : '0.0',
|
||||
label: 'AI Assistant'
|
||||
},
|
||||
human: {
|
||||
total: humanDecisions.length,
|
||||
blocked: humanBlocked,
|
||||
blockRate: humanDecisions.length > 0 ? ((humanBlocked / humanDecisions.length) * 100).toFixed(1) : '0.0',
|
||||
label: 'Human Direct'
|
||||
}
|
||||
};
|
||||
|
||||
// Framework Maturity Score (0-100)
|
||||
// Based on: block rate trend, severity distribution, learning curve
|
||||
const recentDays = 7;
|
||||
const recentDate = new Date();
|
||||
recentDate.setDate(recentDate.getDate() - recentDays);
|
||||
const recentDecisions = decisions.filter(d => new Date(d.timestamp) >= recentDate);
|
||||
const recentBlocked = recentDecisions.filter(d => !d.allowed).length;
|
||||
const recentBlockRate = recentDecisions.length > 0 ? (recentBlocked / recentDecisions.length) : 0;
|
||||
|
||||
// Lower block rate = higher maturity (framework teaching good habits)
|
||||
const blockRateScore = Math.max(0, 100 - (recentBlockRate * 1000));
|
||||
|
||||
// More AI decisions with low block rate = higher maturity
|
||||
const aiAdoptionScore = aiDecisions.length > 0 ? ((aiDecisions.length / decisions.length) * 100) : 0;
|
||||
|
||||
// Fewer critical/high severity blocks = higher maturity
|
||||
const criticalCount = blockedDecisions.filter(d =>
|
||||
d.violations && d.violations.some(v => v.severity === 'CRITICAL' || v.severity === 'HIGH')
|
||||
).length;
|
||||
const severityScore = Math.max(0, 100 - (criticalCount / decisions.length * 1000));
|
||||
|
||||
analytics.frameworkMaturity = {
|
||||
score: Math.round((blockRateScore + aiAdoptionScore + severityScore) / 3),
|
||||
components: {
|
||||
blockRateScore: Math.round(blockRateScore),
|
||||
aiAdoptionScore: Math.round(aiAdoptionScore),
|
||||
severityScore: Math.round(severityScore)
|
||||
},
|
||||
trend: recentBlockRate < avgBlockRate ? 'improving' : 'stable',
|
||||
message: recentBlockRate < 0.05 ? 'Excellent - Framework teaching good practices' :
|
||||
recentBlockRate < 0.10 ? 'Good - Team adapting well to governance' :
|
||||
'Learning - Framework actively preventing violations'
|
||||
};
|
||||
|
||||
// ROI Projections
|
||||
const totalDecisions = decisions.length;
|
||||
const totalBlocks = blockedDecisions.length;
|
||||
const avgBlockRate = totalDecisions > 0 ? (totalBlocks / totalDecisions) : 0;
|
||||
|
||||
analytics.roiProjections = {
|
||||
// Current period metrics
|
||||
decisionsPerDay: days > 0 ? (totalDecisions / days).toFixed(1) : '0',
|
||||
blocksPerDay: days > 0 ? (totalBlocks / days).toFixed(1) : '0',
|
||||
|
||||
// Projections for scaled deployment
|
||||
projections: [
|
||||
{
|
||||
userCount: 1000,
|
||||
decisionsPerMonth: Math.round((totalDecisions / days) * 30 * 1000 / decisions.length * 10), // Estimated
|
||||
blocksPerMonth: Math.round((totalBlocks / days) * 30 * 1000 / decisions.length * 10),
|
||||
highSeverityBlocks: Math.round(analytics.frameworkSaves.length / days * 30 * 1000 / decisions.length * 10),
|
||||
businessImpactSaved: Math.round(analytics.businessImpactTotal / days * 30 * 1000 / decisions.length * 10)
|
||||
},
|
||||
{
|
||||
userCount: 10000,
|
||||
decisionsPerMonth: Math.round((totalDecisions / days) * 30 * 10000 / decisions.length * 10),
|
||||
blocksPerMonth: Math.round((totalBlocks / days) * 30 * 10000 / decisions.length * 10),
|
||||
highSeverityBlocks: Math.round(analytics.frameworkSaves.length / days * 30 * 10000 / decisions.length * 10),
|
||||
businessImpactSaved: Math.round(analytics.businessImpactTotal / days * 30 * 10000 / decisions.length * 10)
|
||||
},
|
||||
{
|
||||
userCount: 70000,
|
||||
decisionsPerMonth: Math.round((totalDecisions / days) * 30 * 70000 / decisions.length * 10),
|
||||
blocksPerMonth: Math.round((totalBlocks / days) * 30 * 70000 / decisions.length * 10),
|
||||
highSeverityBlocks: Math.round(analytics.frameworkSaves.length / days * 30 * 70000 / decisions.length * 10),
|
||||
businessImpactSaved: Math.round(analytics.businessImpactTotal / days * 30 * 70000 / decisions.length * 10)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
analytics
|
||||
|
|
@ -163,7 +451,69 @@ async function getAuditAnalytics(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cost configuration
|
||||
* GET /api/admin/cost-config
|
||||
*/
|
||||
async function getCostConfig(req, res) {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
costFactors: userCostFactors
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching cost config:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cost configuration
|
||||
* POST /api/admin/cost-config
|
||||
*/
|
||||
async function updateCostConfig(req, res) {
|
||||
try {
|
||||
const { costFactors } = req.body;
|
||||
|
||||
if (!costFactors) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'costFactors required'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate and update
|
||||
['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].forEach(severity => {
|
||||
if (costFactors[severity]) {
|
||||
userCostFactors[severity] = {
|
||||
amount: parseFloat(costFactors[severity].amount) || userCostFactors[severity].amount,
|
||||
currency: costFactors[severity].currency || userCostFactors[severity].currency,
|
||||
rationale: costFactors[severity].rationale || userCostFactors[severity].rationale
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Cost factors updated by', req.user?.username || 'admin');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
costFactors: userCostFactors
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error updating cost config:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAuditLogs,
|
||||
getAuditAnalytics
|
||||
getAuditAnalytics,
|
||||
getCostConfig,
|
||||
updateCostConfig
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,4 +38,18 @@ router.get('/audit-analytics',
|
|||
auditController.getAuditAnalytics
|
||||
);
|
||||
|
||||
// Get cost configuration (admin only)
|
||||
router.get('/cost-config',
|
||||
authenticateToken,
|
||||
requireRole('admin'),
|
||||
auditController.getCostConfig
|
||||
);
|
||||
|
||||
// Update cost configuration (admin only)
|
||||
router.post('/cost-config',
|
||||
authenticateToken,
|
||||
requireRole('admin'),
|
||||
auditController.updateCostConfig
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
236
src/utils/activity-classifier.util.js
Normal file
236
src/utils/activity-classifier.util.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Copyright 2025 John G Stroh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Activity Classifier
|
||||
* Provides business intelligence context for governance decisions
|
||||
* Enables ROI analysis and use-case tracking
|
||||
*/
|
||||
|
||||
// Activity type definitions
|
||||
const ACTIVITY_TYPES = {
|
||||
HUMAN_INITIATED: 'Human-Initiated Command',
|
||||
AUTONOMOUS_PROCESSING: 'Autonomous Processing',
|
||||
CODE_GENERATION: 'Code Generation',
|
||||
DOCUMENTATION: 'Documentation',
|
||||
CLIENT_COMMUNICATION: 'Client Communication',
|
||||
DEPLOYMENT: 'Deployment',
|
||||
COMPLIANCE_REVIEW: 'Compliance Review',
|
||||
DATA_MANAGEMENT: 'Data Management'
|
||||
};
|
||||
|
||||
// Risk context levels
|
||||
const RISK_LEVELS = {
|
||||
MINIMAL: 'minimal', // No external impact
|
||||
LOW: 'low', // Internal team only
|
||||
MEDIUM: 'medium', // Organization-wide
|
||||
HIGH: 'high', // Client/public facing
|
||||
CRITICAL: 'critical' // Legal/financial/regulatory
|
||||
};
|
||||
|
||||
// Stakeholder impact
|
||||
const STAKEHOLDER_IMPACT = {
|
||||
INDIVIDUAL: 'individual', // Single developer
|
||||
TEAM: 'team', // Development team
|
||||
ORGANIZATION: 'organization', // Entire organization
|
||||
CLIENT: 'client', // External clients
|
||||
PUBLIC: 'public' // Public audience
|
||||
};
|
||||
|
||||
// Data sensitivity levels
|
||||
const DATA_SENSITIVITY = {
|
||||
PUBLIC: 'public',
|
||||
INTERNAL: 'internal',
|
||||
CONFIDENTIAL: 'confidential',
|
||||
RESTRICTED: 'restricted'
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify activity based on action and metadata
|
||||
*/
|
||||
function classifyActivity(action, metadata = {}) {
|
||||
const filePath = metadata.filePath || metadata.file || '';
|
||||
const description = metadata.description || metadata.reason || '';
|
||||
const service = metadata.service || '';
|
||||
|
||||
let activityType = ACTIVITY_TYPES.AUTONOMOUS_PROCESSING;
|
||||
let riskLevel = RISK_LEVELS.LOW;
|
||||
let stakeholderImpact = STAKEHOLDER_IMPACT.INDIVIDUAL;
|
||||
let dataSensitivity = DATA_SENSITIVITY.INTERNAL;
|
||||
|
||||
// Detect activity type
|
||||
if (action === 'file_edit_hook' || action === 'file_write_hook') {
|
||||
// Check file path patterns
|
||||
if (filePath.includes('public/') && !filePath.includes('admin/')) {
|
||||
activityType = ACTIVITY_TYPES.CLIENT_COMMUNICATION;
|
||||
stakeholderImpact = STAKEHOLDER_IMPACT.PUBLIC;
|
||||
riskLevel = RISK_LEVELS.HIGH;
|
||||
dataSensitivity = DATA_SENSITIVITY.PUBLIC;
|
||||
} else if (filePath.includes('docs/')) {
|
||||
activityType = ACTIVITY_TYPES.DOCUMENTATION;
|
||||
stakeholderImpact = STAKEHOLDER_IMPACT.ORGANIZATION;
|
||||
riskLevel = RISK_LEVELS.MEDIUM;
|
||||
} else if (filePath.includes('.js') || filePath.includes('.css') || filePath.includes('.html')) {
|
||||
activityType = ACTIVITY_TYPES.CODE_GENERATION;
|
||||
stakeholderImpact = STAKEHOLDER_IMPACT.TEAM;
|
||||
riskLevel = RISK_LEVELS.LOW;
|
||||
} else if (filePath.includes('deploy') || filePath.includes('production')) {
|
||||
activityType = ACTIVITY_TYPES.DEPLOYMENT;
|
||||
stakeholderImpact = STAKEHOLDER_IMPACT.ORGANIZATION;
|
||||
riskLevel = RISK_LEVELS.HIGH;
|
||||
}
|
||||
} else if (service === 'BoundaryEnforcer') {
|
||||
activityType = ACTIVITY_TYPES.COMPLIANCE_REVIEW;
|
||||
riskLevel = RISK_LEVELS.MEDIUM;
|
||||
} else if (service === 'ContextPressureMonitor' || service === 'MetacognitiveVerifier') {
|
||||
activityType = ACTIVITY_TYPES.AUTONOMOUS_PROCESSING;
|
||||
}
|
||||
|
||||
// Detect client-facing content
|
||||
if (filePath.includes('/components/footer') ||
|
||||
filePath.includes('/components/navbar') ||
|
||||
filePath.includes('contact') ||
|
||||
filePath.includes('email')) {
|
||||
activityType = ACTIVITY_TYPES.CLIENT_COMMUNICATION;
|
||||
stakeholderImpact = STAKEHOLDER_IMPACT.CLIENT;
|
||||
riskLevel = RISK_LEVELS.HIGH;
|
||||
}
|
||||
|
||||
// Detect credentials and sensitive data
|
||||
if (description.toLowerCase().includes('credential') ||
|
||||
description.toLowerCase().includes('password') ||
|
||||
description.toLowerCase().includes('api key') ||
|
||||
description.toLowerCase().includes('secret')) {
|
||||
dataSensitivity = DATA_SENSITIVITY.RESTRICTED;
|
||||
riskLevel = RISK_LEVELS.CRITICAL;
|
||||
}
|
||||
|
||||
// Detect copyright/legal
|
||||
if (description.toLowerCase().includes('copyright') ||
|
||||
description.toLowerCase().includes('license') ||
|
||||
filePath.includes('LICENSE')) {
|
||||
riskLevel = RISK_LEVELS.CRITICAL;
|
||||
stakeholderImpact = STAKEHOLDER_IMPACT.PUBLIC;
|
||||
}
|
||||
|
||||
return {
|
||||
activityType,
|
||||
riskLevel,
|
||||
stakeholderImpact,
|
||||
dataSensitivity,
|
||||
reversibility: calculateReversibility(activityType, stakeholderImpact)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate reversibility of action
|
||||
*/
|
||||
function calculateReversibility(activityType, stakeholderImpact) {
|
||||
// Public-facing and client communication is harder to reverse
|
||||
if (stakeholderImpact === STAKEHOLDER_IMPACT.PUBLIC ||
|
||||
stakeholderImpact === STAKEHOLDER_IMPACT.CLIENT) {
|
||||
return 'difficult';
|
||||
}
|
||||
|
||||
// Deployment is harder to reverse
|
||||
if (activityType === ACTIVITY_TYPES.DEPLOYMENT) {
|
||||
return 'moderate';
|
||||
}
|
||||
|
||||
// Code and documentation are easily reversible
|
||||
if (activityType === ACTIVITY_TYPES.CODE_GENERATION ||
|
||||
activityType === ACTIVITY_TYPES.DOCUMENTATION) {
|
||||
return 'easy';
|
||||
}
|
||||
|
||||
return 'moderate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable activity description
|
||||
*/
|
||||
function getActivityDescription(classification) {
|
||||
const { activityType, stakeholderImpact, riskLevel } = classification;
|
||||
|
||||
const descriptions = {
|
||||
[ACTIVITY_TYPES.CODE_GENERATION]: 'Internal development work',
|
||||
[ACTIVITY_TYPES.CLIENT_COMMUNICATION]: 'Client-facing content',
|
||||
[ACTIVITY_TYPES.DOCUMENTATION]: 'Documentation updates',
|
||||
[ACTIVITY_TYPES.DEPLOYMENT]: 'Production deployment',
|
||||
[ACTIVITY_TYPES.COMPLIANCE_REVIEW]: 'Compliance validation',
|
||||
[ACTIVITY_TYPES.AUTONOMOUS_PROCESSING]: 'Background processing',
|
||||
[ACTIVITY_TYPES.DATA_MANAGEMENT]: 'Data operations'
|
||||
};
|
||||
|
||||
return descriptions[activityType] || 'Unknown activity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate business impact score (0-100)
|
||||
*/
|
||||
function calculateBusinessImpact(classification, wasBlocked) {
|
||||
let score = 0;
|
||||
|
||||
// Risk level contribution (0-40)
|
||||
const riskScores = {
|
||||
[RISK_LEVELS.MINIMAL]: 5,
|
||||
[RISK_LEVELS.LOW]: 10,
|
||||
[RISK_LEVELS.MEDIUM]: 20,
|
||||
[RISK_LEVELS.HIGH]: 30,
|
||||
[RISK_LEVELS.CRITICAL]: 40
|
||||
};
|
||||
score += riskScores[classification.riskLevel] || 10;
|
||||
|
||||
// Stakeholder impact contribution (0-30)
|
||||
const stakeholderScores = {
|
||||
[STAKEHOLDER_IMPACT.INDIVIDUAL]: 5,
|
||||
[STAKEHOLDER_IMPACT.TEAM]: 10,
|
||||
[STAKEHOLDER_IMPACT.ORGANIZATION]: 20,
|
||||
[STAKEHOLDER_IMPACT.CLIENT]: 25,
|
||||
[STAKEHOLDER_IMPACT.PUBLIC]: 30
|
||||
};
|
||||
score += stakeholderScores[classification.stakeholderImpact] || 10;
|
||||
|
||||
// Data sensitivity contribution (0-20)
|
||||
const sensitivityScores = {
|
||||
[DATA_SENSITIVITY.PUBLIC]: 5,
|
||||
[DATA_SENSITIVITY.INTERNAL]: 10,
|
||||
[DATA_SENSITIVITY.CONFIDENTIAL]: 15,
|
||||
[DATA_SENSITIVITY.RESTRICTED]: 20
|
||||
};
|
||||
score += sensitivityScores[classification.dataSensitivity] || 5;
|
||||
|
||||
// Reversibility contribution (0-10)
|
||||
const reversibilityScores = {
|
||||
'easy': 2,
|
||||
'moderate': 5,
|
||||
'difficult': 10
|
||||
};
|
||||
score += reversibilityScores[classification.reversibility] || 5;
|
||||
|
||||
// If blocked, this represents value saved
|
||||
return wasBlocked ? score : 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ACTIVITY_TYPES,
|
||||
RISK_LEVELS,
|
||||
STAKEHOLDER_IMPACT,
|
||||
DATA_SENSITIVITY,
|
||||
classifyActivity,
|
||||
getActivityDescription,
|
||||
calculateBusinessImpact
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue