Framework Service Enhancements: - ContextPressureMonitor: Enhanced statistics tracking and contextual adjustments - InstructionPersistenceClassifier: Improved context integration and consistency - MetacognitiveVerifier: Extended verification capabilities and logging - All services: 182 unit tests passing Admin Interface Improvements: - Blog curation: Enhanced content management and validation - Audit analytics: Improved analytics dashboard and reporting - Dashboard: Updated metrics and visualizations Documentation: - Architectural overview: Improved markdown formatting for readability - Added blank lines between sections for better structure - Fixed table formatting for version history All tests passing: Framework stable for deployment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
671 lines
17 KiB
JavaScript
671 lines
17 KiB
JavaScript
/**
|
|
* Blog Controller
|
|
* AI-curated blog with human oversight
|
|
*/
|
|
|
|
const BlogPost = require('../models/BlogPost.model');
|
|
const ModerationQueue = require('../models/ModerationQueue.model');
|
|
const GovernanceLog = require('../models/GovernanceLog.model');
|
|
const { markdownToHtml } = require('../utils/markdown.util');
|
|
const logger = require('../utils/logger.util');
|
|
const claudeAPI = require('../services/ClaudeAPI.service');
|
|
const BoundaryEnforcer = require('../services/BoundaryEnforcer.service');
|
|
const BlogCuration = require('../services/BlogCuration.service');
|
|
|
|
/**
|
|
* List published blog posts (public)
|
|
* GET /api/blog
|
|
*/
|
|
async function listPublishedPosts(req, res) {
|
|
try {
|
|
const { limit = 10, skip = 0 } = req.query;
|
|
|
|
const posts = await BlogPost.findPublished({
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
|
|
const total = await BlogPost.countByStatus('published');
|
|
|
|
res.json({
|
|
success: true,
|
|
posts,
|
|
pagination: {
|
|
total,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip),
|
|
hasMore: parseInt(skip) + posts.length < total
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List published posts error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get single blog post by slug (public, published only)
|
|
* GET /api/blog/:slug
|
|
*/
|
|
async function getPublishedPost(req, res) {
|
|
try {
|
|
const { slug } = req.params;
|
|
|
|
const post = await BlogPost.findBySlug(slug);
|
|
|
|
if (!post || post.status !== 'published') {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
// Increment view count
|
|
await BlogPost.incrementViews(post._id);
|
|
|
|
res.json({
|
|
success: true,
|
|
post
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get published post error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List posts by status (admin only)
|
|
* GET /api/blog/admin/posts?status=draft
|
|
*/
|
|
async function listPostsByStatus(req, res) {
|
|
try {
|
|
const { status = 'draft', limit = 20, skip = 0 } = req.query;
|
|
|
|
const posts = await BlogPost.findByStatus(status, {
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
|
|
const total = await BlogPost.countByStatus(status);
|
|
|
|
res.json({
|
|
success: true,
|
|
status,
|
|
posts,
|
|
pagination: {
|
|
total,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List posts by status error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get any post by ID (admin only)
|
|
* GET /api/blog/admin/:id
|
|
*/
|
|
async function getPostById(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const post = await BlogPost.findById(id);
|
|
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
post
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get post by ID error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create blog post (admin only)
|
|
* POST /api/blog
|
|
*/
|
|
async function createPost(req, res) {
|
|
try {
|
|
const { title, slug, content, excerpt, tags, author, tractatus_classification } = req.body;
|
|
|
|
// Convert markdown content to HTML if needed
|
|
const content_html = content.includes('# ') ? markdownToHtml(content) : content;
|
|
|
|
const post = await BlogPost.create({
|
|
title,
|
|
slug,
|
|
content: content_html,
|
|
excerpt,
|
|
tags,
|
|
author: {
|
|
...author,
|
|
name: author?.name || req.user.name || req.user.email
|
|
},
|
|
tractatus_classification,
|
|
status: 'draft'
|
|
});
|
|
|
|
logger.info(`Blog post created: ${slug} by ${req.user.email}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
post
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Create post error:', error);
|
|
|
|
// Handle duplicate slug
|
|
if (error.code === 11000) {
|
|
return res.status(409).json({
|
|
error: 'Conflict',
|
|
message: 'A post with this slug already exists'
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update blog post (admin only)
|
|
* PUT /api/blog/:id
|
|
*/
|
|
async function updatePost(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates = { ...req.body };
|
|
|
|
// If content is updated and looks like markdown, convert to HTML
|
|
if (updates.content && updates.content.includes('# ')) {
|
|
updates.content = markdownToHtml(updates.content);
|
|
}
|
|
|
|
const success = await BlogPost.update(id, updates);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
const post = await BlogPost.findById(id);
|
|
|
|
logger.info(`Blog post updated: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
post
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Update post error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Publish blog post (admin only)
|
|
* POST /api/blog/:id/publish
|
|
*/
|
|
async function publishPost(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { review_notes } = req.body;
|
|
|
|
const post = await BlogPost.findById(id);
|
|
|
|
if (!post) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
if (post.status === 'published') {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Post is already published'
|
|
});
|
|
}
|
|
|
|
// Update with review notes if provided
|
|
if (review_notes) {
|
|
await BlogPost.update(id, {
|
|
'moderation.review_notes': review_notes
|
|
});
|
|
}
|
|
|
|
// Publish the post
|
|
await BlogPost.publish(id, req.userId);
|
|
|
|
const updatedPost = await BlogPost.findById(id);
|
|
|
|
logger.info(`Blog post published: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
post: updatedPost,
|
|
message: 'Post published successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Publish post error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete blog post (admin only)
|
|
* DELETE /api/blog/:id
|
|
*/
|
|
async function deletePost(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const success = await BlogPost.delete(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Blog post deleted: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Post deleted successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Delete post error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Suggest blog topics using AI (admin only)
|
|
* POST /api/blog/suggest-topics
|
|
*
|
|
* TRA-OPS-0002: AI suggests topics, human writes and approves posts
|
|
*/
|
|
async function suggestTopics(req, res) {
|
|
try {
|
|
const { audience, theme } = req.body;
|
|
|
|
// Validate audience
|
|
const validAudiences = ['researcher', 'implementer', 'advocate', 'general'];
|
|
if (!audience || !validAudiences.includes(audience)) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: `Audience must be one of: ${validAudiences.join(', ')}`
|
|
});
|
|
}
|
|
|
|
logger.info(`Blog topic suggestion requested: audience=${audience}, theme=${theme || 'none'}`);
|
|
|
|
// 1. Boundary check (TRA-OPS-0002: Editorial decisions require human oversight)
|
|
const boundaryCheck = BoundaryEnforcer.enforce({
|
|
description: 'Suggest blog topics for editorial calendar',
|
|
text: 'AI provides suggestions, human makes final editorial decisions',
|
|
classification: { quadrant: 'OPERATIONAL' },
|
|
type: 'content_suggestion'
|
|
});
|
|
|
|
// Log boundary check
|
|
await GovernanceLog.create({
|
|
action: 'BLOG_TOPIC_SUGGESTION',
|
|
user_id: req.user._id,
|
|
user_email: req.user.email,
|
|
timestamp: new Date(),
|
|
boundary_check: boundaryCheck,
|
|
outcome: boundaryCheck.allowed ? 'QUEUED_FOR_APPROVAL' : 'BLOCKED',
|
|
details: {
|
|
audience,
|
|
theme
|
|
}
|
|
});
|
|
|
|
if (!boundaryCheck.allowed) {
|
|
logger.warn(`Blog topic suggestion blocked by BoundaryEnforcer: ${boundaryCheck.section}`);
|
|
return res.status(403).json({
|
|
error: 'Boundary Violation',
|
|
message: boundaryCheck.reasoning,
|
|
section: boundaryCheck.section,
|
|
details: 'This action requires human judgment in values territory'
|
|
});
|
|
}
|
|
|
|
// 2. Claude API call for topic suggestions
|
|
const suggestions = await claudeAPI.generateBlogTopics(audience, theme);
|
|
|
|
logger.info(`Claude API returned ${suggestions.length} topic suggestions`);
|
|
|
|
// 3. Create moderation queue entry (human approval required)
|
|
const queueEntry = await ModerationQueue.create({
|
|
type: 'BLOG_TOPIC_SUGGESTION',
|
|
reference_collection: 'blog_posts',
|
|
data: {
|
|
audience,
|
|
theme,
|
|
suggestions,
|
|
requested_by: req.user.email
|
|
},
|
|
status: 'PENDING_APPROVAL',
|
|
ai_generated: true,
|
|
requires_human_approval: true,
|
|
created_by: req.user._id,
|
|
created_at: new Date(),
|
|
metadata: {
|
|
boundary_check: boundaryCheck,
|
|
governance_policy: 'TRA-OPS-0002'
|
|
}
|
|
});
|
|
|
|
logger.info(`Created moderation queue entry: ${queueEntry._id}`);
|
|
|
|
// 4. Return response (suggestions queued for human review)
|
|
res.json({
|
|
success: true,
|
|
message: 'Blog topic suggestions generated. Awaiting human review and approval.',
|
|
queue_id: queueEntry._id,
|
|
suggestions,
|
|
governance: {
|
|
policy: 'TRA-OPS-0002',
|
|
boundary_check: boundaryCheck,
|
|
requires_approval: true,
|
|
note: 'Topics are suggestions only. Human must write all blog posts.'
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Suggest topics error:', error);
|
|
|
|
// Handle Claude API errors specifically
|
|
if (error.message.includes('Claude API')) {
|
|
return res.status(502).json({
|
|
error: 'AI Service Error',
|
|
message: 'Failed to generate topic suggestions. Please try again.',
|
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draft a full blog post using AI (admin only)
|
|
* POST /api/blog/draft-post
|
|
*
|
|
* TRA-OPS-0002: AI drafts content, human reviews and approves before publication
|
|
* Enforces inst_016, inst_017, inst_018 via BlogCuration service
|
|
*/
|
|
async function draftBlogPost(req, res) {
|
|
try {
|
|
const { topic, audience, length = 'medium', focus } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!topic || !audience) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'topic and audience are required'
|
|
});
|
|
}
|
|
|
|
const validAudiences = ['researcher', 'implementer', 'advocate', 'general'];
|
|
if (!validAudiences.includes(audience)) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: `audience must be one of: ${validAudiences.join(', ')}`
|
|
});
|
|
}
|
|
|
|
const validLengths = ['short', 'medium', 'long'];
|
|
if (!validLengths.includes(length)) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: `length must be one of: ${validLengths.join(', ')}`
|
|
});
|
|
}
|
|
|
|
logger.info(`Blog post draft requested: topic="${topic}", audience=${audience}, length=${length}`);
|
|
|
|
// Generate draft using BlogCuration service (includes boundary checks and validation)
|
|
const result = await BlogCuration.draftBlogPost({
|
|
topic,
|
|
audience,
|
|
length,
|
|
focus
|
|
});
|
|
|
|
const { draft, validation, boundary_check, metadata } = result;
|
|
|
|
// Log governance action
|
|
await GovernanceLog.create({
|
|
action: 'BLOG_POST_DRAFT',
|
|
user_id: req.user._id,
|
|
user_email: req.user.email,
|
|
timestamp: new Date(),
|
|
boundary_check,
|
|
outcome: 'QUEUED_FOR_APPROVAL',
|
|
details: {
|
|
topic,
|
|
audience,
|
|
length,
|
|
validation_result: validation.recommendation,
|
|
violations: validation.violations.length,
|
|
warnings: validation.warnings.length
|
|
}
|
|
});
|
|
|
|
// Create moderation queue entry (human approval required)
|
|
const queueEntry = await ModerationQueue.create({
|
|
type: 'BLOG_POST_DRAFT',
|
|
reference_collection: 'blog_posts',
|
|
data: {
|
|
topic,
|
|
audience,
|
|
length,
|
|
focus,
|
|
draft,
|
|
validation,
|
|
requested_by: req.user.email
|
|
},
|
|
status: 'PENDING_APPROVAL',
|
|
ai_generated: true,
|
|
requires_human_approval: true,
|
|
created_by: req.user._id,
|
|
created_at: new Date(),
|
|
priority: validation.violations.length > 0 ? 'high' : 'medium',
|
|
metadata: {
|
|
boundary_check,
|
|
governance_policy: 'TRA-OPS-0002',
|
|
tractatus_instructions: ['inst_016', 'inst_017', 'inst_018'],
|
|
model_info: metadata
|
|
}
|
|
});
|
|
|
|
logger.info(`Created blog draft queue entry: ${queueEntry._id}, validation: ${validation.recommendation}`);
|
|
|
|
// Return response
|
|
res.json({
|
|
success: true,
|
|
message: 'Blog post draft generated. Awaiting human review and approval.',
|
|
queue_id: queueEntry._id,
|
|
draft,
|
|
validation,
|
|
governance: {
|
|
policy: 'TRA-OPS-0002',
|
|
boundary_check,
|
|
requires_approval: true,
|
|
tractatus_enforcement: {
|
|
inst_016: 'No fabricated statistics or unverifiable claims',
|
|
inst_017: 'No absolute assurance terms (guarantee, 100%, etc.)',
|
|
inst_018: 'No unverified production-ready claims'
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Draft blog post error:', error);
|
|
|
|
// Handle boundary violations
|
|
if (error.message.includes('Boundary violation')) {
|
|
return res.status(403).json({
|
|
error: 'Boundary Violation',
|
|
message: error.message
|
|
});
|
|
}
|
|
|
|
// Handle Claude API errors
|
|
if (error.message.includes('Claude API') || error.message.includes('Blog draft generation failed')) {
|
|
return res.status(502).json({
|
|
error: 'AI Service Error',
|
|
message: 'Failed to generate blog draft. Please try again.',
|
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze blog content for Tractatus compliance (admin only)
|
|
* POST /api/blog/analyze-content
|
|
*
|
|
* Validates content against inst_016, inst_017, inst_018
|
|
*/
|
|
async function analyzeContent(req, res) {
|
|
try {
|
|
const { title, body } = req.body;
|
|
|
|
if (!title || !body) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'title and body are required'
|
|
});
|
|
}
|
|
|
|
logger.info(`Content compliance analysis requested: "${title}"`);
|
|
|
|
const analysis = await BlogCuration.analyzeContentCompliance({
|
|
title,
|
|
body
|
|
});
|
|
|
|
logger.info(`Compliance analysis complete: ${analysis.recommendation}, score: ${analysis.overall_score}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
analysis,
|
|
tractatus_enforcement: {
|
|
inst_016: 'No fabricated statistics',
|
|
inst_017: 'No absolute guarantees',
|
|
inst_018: 'No unverified production claims'
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Analyze content error:', error);
|
|
|
|
if (error.message.includes('Claude API') || error.message.includes('Compliance analysis failed')) {
|
|
return res.status(502).json({
|
|
error: 'AI Service Error',
|
|
message: 'Failed to analyze content. Please try again.',
|
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
});
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get editorial guidelines (admin only)
|
|
* GET /api/blog/editorial-guidelines
|
|
*/
|
|
async function getEditorialGuidelines(req, res) {
|
|
try {
|
|
const guidelines = BlogCuration.getEditorialGuidelines();
|
|
|
|
res.json({
|
|
success: true,
|
|
guidelines
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get editorial guidelines error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
listPublishedPosts,
|
|
getPublishedPost,
|
|
listPostsByStatus,
|
|
getPostById,
|
|
createPost,
|
|
updatePost,
|
|
publishPost,
|
|
deletePost,
|
|
suggestTopics,
|
|
draftBlogPost,
|
|
analyzeContent,
|
|
getEditorialGuidelines
|
|
};
|