- audit.controller.js: Remove unused fs/path imports, add AuditLog import, fix indentation, use const for userCostFactors, use property shorthand - crm.controller.js: Remove unused Contact, MediaInquiry, CaseSubmission imports - cases.controller.js: Remove unused GovernanceLog, BoundaryEnforcer imports - DiskMetrics.model.js: Use template literals instead of string concatenation - framework-content-analysis.controller.js: Use template literals, prefix unused destructured vars with underscore - feedback.controller.js: Use template literal for string concat - DeliberationSession.model.js: Fix line length by moving comments to own lines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
553 lines
14 KiB
JavaScript
553 lines
14 KiB
JavaScript
/**
|
|
* Feedback Controller
|
|
* Handle feedback submissions with Tractatus governance
|
|
*/
|
|
|
|
const Feedback = require('../models/Feedback.model');
|
|
const {
|
|
BoundaryEnforcer,
|
|
PluralisticDeliberator,
|
|
CrossReferenceValidator
|
|
} = require('../services/feedback-governance.service');
|
|
const logger = require('../utils/logger.util');
|
|
const { getClientIp } = require('../utils/security-logger');
|
|
|
|
// Initialize governance components
|
|
const boundaryEnforcer = new BoundaryEnforcer();
|
|
const deliberator = new PluralisticDeliberator();
|
|
const validator = new CrossReferenceValidator();
|
|
|
|
/**
|
|
* POST /api/feedback/submit
|
|
* Submit feedback with automatic governance classification
|
|
*/
|
|
async function submit(req, res) {
|
|
try {
|
|
const { type, content, name, email } = req.body;
|
|
|
|
// Step 1: Classify feedback and determine governance pathway
|
|
const classification = boundaryEnforcer.classify(type, content);
|
|
|
|
// Step 2: Validate classification
|
|
const classificationValidation = boundaryEnforcer.validate(classification);
|
|
if (!classificationValidation.valid) {
|
|
logger.error('[Feedback] Invalid classification:', classificationValidation.error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: 'Classification error',
|
|
message: 'Unable to classify feedback. Please try again.'
|
|
});
|
|
}
|
|
|
|
// Step 3: Create feedback record with governance data
|
|
const feedback = await Feedback.create({
|
|
type,
|
|
pathway: classification.pathway,
|
|
content,
|
|
submittedBy: { name, email },
|
|
metadata: {
|
|
user_agent: req.get('user-agent'),
|
|
ip: getClientIp(req),
|
|
source_page: req.get('referer'),
|
|
referrer: req.get('referer')
|
|
},
|
|
governance: {
|
|
boundaryCheck: 'completed',
|
|
deliberationRequired: classification.pathway === 'deliberation',
|
|
stakeholders: classification.stakeholders || [],
|
|
constraints: classification.constraints,
|
|
classificationReason: classification.reason
|
|
}
|
|
});
|
|
|
|
// Step 4: If deliberation required, initiate deliberation process
|
|
if (classification.pathway === 'deliberation') {
|
|
const deliberation = await deliberator.initiate(
|
|
feedback.feedbackId,
|
|
classification.stakeholders,
|
|
content
|
|
);
|
|
|
|
await Feedback.update(feedback.feedbackId, {
|
|
'governance.deliberationId': deliberation.deliberationId,
|
|
'governance.deliberationStatus': 'awaiting_votes',
|
|
'governance.deliberationDeadline': deliberation.deadline
|
|
});
|
|
|
|
logger.info(`[Feedback] Deliberation initiated: ${deliberation.deliberationId}`);
|
|
}
|
|
|
|
logger.info(`[Feedback] New submission: ${feedback.feedbackId} | Pathway: ${classification.pathway}`);
|
|
|
|
// Step 5: Return appropriate response based on pathway
|
|
const messages = {
|
|
autonomous: 'Thank you! An AI assistant will respond shortly with relevant information.',
|
|
deliberation: 'Thank you! Your feedback requires multi-stakeholder review. You will receive a response within 72 hours.',
|
|
human_mandatory: 'Thank you! Your inquiry requires personal attention. You will receive a response within 48 hours.'
|
|
};
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: messages[classification.pathway],
|
|
feedbackId: feedback.feedbackId,
|
|
pathway: classification.pathway,
|
|
trackingUrl: `/api/feedback/status/${feedback.feedbackId}`
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Submit error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to submit feedback',
|
|
message: 'An error occurred. Please try again.'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/feedback/status/:feedbackId
|
|
* Check status of feedback submission (public)
|
|
*/
|
|
async function getStatus(req, res) {
|
|
try {
|
|
const { feedbackId } = req.params;
|
|
|
|
const feedback = await Feedback.findById(feedbackId);
|
|
|
|
if (!feedback) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Feedback not found'
|
|
});
|
|
}
|
|
|
|
// Return limited public information
|
|
res.json({
|
|
success: true,
|
|
feedbackId: feedback.feedbackId,
|
|
status: feedback.status,
|
|
pathway: feedback.pathway,
|
|
submittedAt: feedback.created_at,
|
|
hasResponse: !!feedback.response.content,
|
|
responsePreview: feedback.response.content
|
|
? `${feedback.response.content.substring(0, 200)}...`
|
|
: null
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Get status error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch status'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/feedback/admin/stats
|
|
* Get feedback statistics (admin)
|
|
*/
|
|
async function getStats(req, res) {
|
|
try {
|
|
const stats = await Feedback.getStatistics();
|
|
|
|
res.json({
|
|
success: true,
|
|
stats
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Stats error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch statistics'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/feedback/admin/queue
|
|
* Get feedback queue filtered by pathway
|
|
*/
|
|
async function getQueue(req, res) {
|
|
try {
|
|
const { pathway, status = 'pending', limit = 20, skip = 0 } = req.query;
|
|
|
|
if (!pathway) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Pathway parameter required (autonomous, deliberation, human_mandatory)'
|
|
});
|
|
}
|
|
|
|
const feedback = await Feedback.findByPathway(pathway, {
|
|
status,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
pathway,
|
|
queue: feedback,
|
|
count: feedback.length
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Queue error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch queue'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/feedback/admin/list
|
|
* List all feedback with filtering
|
|
*/
|
|
async function list(req, res) {
|
|
try {
|
|
const { status, pathway, limit = 20, skip = 0 } = req.query;
|
|
|
|
let feedback;
|
|
if (pathway) {
|
|
feedback = await Feedback.findByPathway(pathway, {
|
|
status,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
} else if (status) {
|
|
feedback = await Feedback.findByStatus(status, {
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
} else {
|
|
feedback = await Feedback.getRecent(parseInt(limit));
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
feedback,
|
|
count: feedback.length
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] List error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch feedback'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/feedback/admin/:id
|
|
* Get single feedback by ID
|
|
*/
|
|
async function getById(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const feedback = await Feedback.findById(id);
|
|
|
|
if (!feedback) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Feedback not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
feedback
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Get by ID error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch feedback'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/feedback/admin/:id/response
|
|
* Add response to feedback (human or AI)
|
|
*/
|
|
async function addResponse(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { content, respondedBy } = req.body;
|
|
|
|
const feedback = await Feedback.findById(id);
|
|
|
|
if (!feedback) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Feedback not found'
|
|
});
|
|
}
|
|
|
|
// If AI response, validate against constraints
|
|
let validationResult = null;
|
|
if (respondedBy === 'ai') {
|
|
validationResult = validator.validate(content, feedback.governance.constraints);
|
|
|
|
if (!validationResult.valid) {
|
|
const report = validator.getReport(validationResult);
|
|
|
|
if (report.critical > 0) {
|
|
logger.warn('[Feedback] AI response blocked - critical violations:', validationResult.violations);
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'AI response violates governance constraints',
|
|
violations: validationResult.violations,
|
|
recommendation: report.recommendation
|
|
});
|
|
}
|
|
|
|
// Warnings allowed but logged
|
|
logger.warn('[Feedback] AI response has warnings:', validationResult.violations);
|
|
}
|
|
}
|
|
|
|
// Add response to feedback
|
|
await Feedback.addResponse(id, {
|
|
content,
|
|
respondedBy, // 'ai', 'human', or 'deliberated'
|
|
validatedBy: respondedBy === 'ai' ? 'CrossReferenceValidator' : null,
|
|
validationStatus: validationResult ? (validationResult.valid ? 'pass' : 'warning') : null
|
|
});
|
|
|
|
logger.info(`[Feedback] Response added to ${feedback.feedbackId} by ${respondedBy}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Response added successfully',
|
|
validation: validationResult ? validator.getReport(validationResult) : null
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Add response error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to add response'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/feedback/admin/:id/deliberate
|
|
* Initiate deliberation process for feedback
|
|
*/
|
|
async function initiateDeliberation(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { stakeholders } = req.body;
|
|
|
|
const feedback = await Feedback.findById(id);
|
|
|
|
if (!feedback) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Feedback not found'
|
|
});
|
|
}
|
|
|
|
if (feedback.pathway !== 'deliberation') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Feedback does not require deliberation',
|
|
currentPathway: feedback.pathway
|
|
});
|
|
}
|
|
|
|
const deliberation = await deliberator.initiate(
|
|
feedback.feedbackId,
|
|
stakeholders || feedback.governance.stakeholders,
|
|
feedback.content
|
|
);
|
|
|
|
await Feedback.update(id, {
|
|
'governance.deliberationId': deliberation.deliberationId,
|
|
'governance.deliberationStatus': 'awaiting_votes',
|
|
'governance.deliberationDeadline': deliberation.deadline,
|
|
status: 'in_progress'
|
|
});
|
|
|
|
logger.info(`[Feedback] Deliberation initiated: ${deliberation.deliberationId}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Deliberation initiated',
|
|
deliberation
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Initiate deliberation error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to initiate deliberation'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/feedback/admin/deliberation/:deliberationId/vote
|
|
* Submit vote for deliberation
|
|
*/
|
|
async function submitVote(req, res) {
|
|
try {
|
|
const { deliberationId } = req.params;
|
|
const { vote, constraints } = req.body;
|
|
|
|
// In production, this would:
|
|
// 1. Record vote in database
|
|
// 2. Check if all stakeholders voted
|
|
// 3. If complete, process deliberation result
|
|
// 4. Update feedback with deliberation outcome
|
|
|
|
// For now, simulated vote processing
|
|
const votes = [
|
|
{ stakeholder: req.user.id, vote, constraints }
|
|
// In production, fetch all votes from database
|
|
];
|
|
|
|
const result = await deliberator.process(deliberationId, votes);
|
|
|
|
logger.info(`[Feedback] Vote submitted for ${deliberationId}: ${vote}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Vote recorded',
|
|
deliberationResult: result
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Submit vote error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to submit vote'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/feedback/ai/generate-response
|
|
* AI generates response and validates against constraints
|
|
*/
|
|
async function validateAIResponse(req, res) {
|
|
try {
|
|
const { feedbackId, aiResponse } = req.body;
|
|
|
|
const feedback = await Feedback.findById(feedbackId);
|
|
|
|
if (!feedback) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Feedback not found'
|
|
});
|
|
}
|
|
|
|
// Validate AI response against constraints
|
|
const validationResult = validator.validate(aiResponse, feedback.governance.constraints);
|
|
const report = validator.getReport(validationResult);
|
|
|
|
res.json({
|
|
success: true,
|
|
validation: validationResult,
|
|
report,
|
|
approved: report.critical === 0
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Validate AI response error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to validate AI response'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/feedback/admin/:id
|
|
* Update feedback
|
|
*/
|
|
async function update(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { status, priority } = req.body;
|
|
|
|
const updateData = {};
|
|
if (status) updateData.status = status;
|
|
if (priority) updateData.priority = priority;
|
|
|
|
await Feedback.update(id, updateData);
|
|
|
|
logger.info(`[Feedback] Updated ${id}:`, updateData);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Feedback updated successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Update error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to update feedback'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/feedback/admin/:id
|
|
* Delete feedback
|
|
*/
|
|
async function deleteFeedback(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const feedback = await Feedback.findById(id);
|
|
|
|
if (!feedback) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Feedback not found'
|
|
});
|
|
}
|
|
|
|
// In production, implement soft delete or archive
|
|
// For now, log the deletion request
|
|
logger.warn(`[Feedback] Delete requested for ${feedback.feedbackId}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Feedback deletion logged (implement soft delete in production)'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('[Feedback] Delete error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to delete feedback'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
submit,
|
|
getStatus,
|
|
getStats,
|
|
getQueue,
|
|
list,
|
|
getById,
|
|
addResponse,
|
|
initiateDeliberation,
|
|
submitVote,
|
|
validateAIResponse,
|
|
update,
|
|
deleteFeedback
|
|
};
|