tractatus/src/controllers/feedback.controller.js
TheFlow 62b9b1fa32 fix: Resolve ESLint errors breaking CI
- 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>
2026-01-23 12:20:50 +13:00

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
};