diff --git a/scripts/hook-validators/validate-file-edit.js b/scripts/hook-validators/validate-file-edit.js index 47c2c25f..50a874ff 100755 --- a/scripts/hook-validators/validate-file-edit.js +++ b/scripts/hook-validators/validate-file-edit.js @@ -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 }); diff --git a/src/controllers/audit.controller.js b/src/controllers/audit.controller.js index 680ab17a..2c4a0f4d 100644 --- a/src/controllers/audit.controller.js +++ b/src/controllers/audit.controller.js @@ -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 }; diff --git a/src/routes/audit.routes.js b/src/routes/audit.routes.js index 4aef52e7..fa03671e 100644 --- a/src/routes/audit.routes.js +++ b/src/routes/audit.routes.js @@ -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; diff --git a/src/utils/activity-classifier.util.js b/src/utils/activity-classifier.util.js new file mode 100644 index 00000000..3e396739 --- /dev/null +++ b/src/utils/activity-classifier.util.js @@ -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 +};