tractatus/src/services/ContentAnalyzer.service.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

334 lines
10 KiB
JavaScript

/**
* Content Analyzer Service
* Analyzes existing articles for tone, audience fit, themes, and publication suitability
*/
const ClaudeAPIService = require('./ClaudeAPI.service');
const publicationConfig = require('../config/publication-targets.config');
const logger = require('../utils/logger.util');
class ContentAnalyzerService {
/**
* Analyze article content for quality and publication fit
* @param {Object} article - Article to analyze
* @param {string} article.title - Article title
* @param {string} article.content - Article content
* @param {number} article.wordCount - Word count
* @param {string} article.targetPublication - Publication ID (optional)
* @returns {Promise<Object>} Analysis results
*/
async analyzeArticle(article) {
logger.info(`Analyzing article: ${article.title}`);
try {
const claudeAPI = ClaudeAPIService;
// Build system prompt for analysis
const systemPrompt = `You are an expert content analyst evaluating articles about AI governance and the Tractatus framework.
Your task is to analyze the provided article and return a comprehensive assessment in JSON format.
Analyze for:
1. **Tone**: strategic, conversational, academic, investigative, persuasive
2. **Primary Audience**: leader, research, general, technical, policy
3. **Key Themes**: Extract 3-5 main themes discussed
4. **Strengths**: What works well (2-3 points)
5. **Weaknesses**: What could be improved (2-3 points)
6. **Publication Suitability**: If target publication specified, assess fit (1-10 score)
Return JSON:
{
"tone": {
"primary": "strategic|conversational|academic|investigative|persuasive",
"secondary": "strategic|conversational|academic|investigative|persuasive",
"confidence": 0.0-1.0
},
"audience": {
"primary": "leader|research|general|technical|policy",
"secondary": "leader|research|general|technical|policy",
"readingLevel": "undergraduate|postgraduate|professional|general",
"confidence": 0.0-1.0
},
"themes": [
{
"theme": "Theme name",
"prominence": 0.0-1.0,
"description": "Brief description"
}
],
"strengths": [
"Strength description"
],
"weaknesses": [
"Weakness description with specific suggestion"
],
"publicationFit": {
"score": 1-10,
"reasoning": "Why this score",
"recommendations": ["Specific changes to improve fit"]
},
"tractatus": {
"frameworkAlignment": 0.0-1.0,
"quadrant": "STRATEGIC|OPERATIONAL|TACTICAL|MAINTENANCE",
"valuesSensitive": boolean,
"concerns": ["Any concerns about framework representation"]
}
}`;
// Build user prompt with article details
let userPrompt = `Analyze this article:
**Title**: ${article.title}
**Word Count**: ${article.wordCount}
${article.targetPublication ? `**Target Publication**: ${this._getPublicationName(article.targetPublication)}\n` : ''}
**Content**:
${article.content}
Provide comprehensive analysis in JSON format.`;
// If target publication specified, include readership profile
if (article.targetPublication) {
const publication = publicationConfig.getPublicationById(article.targetPublication);
if (publication && publication.readership) {
userPrompt += `\n\n**Target Publication Profile**:
- Name: ${publication.name}
- Type: ${publication.type}
- Primary Audience: ${publication.readership.primary}
- Reader Interests: ${publication.readership.demographics.interests.join(', ')}
- Editorial Tone: ${publication.editorial.tone.join(', ')}
- Preferred Themes: ${publication.readership.contentPreferences.topicThemes.join(', ')}
Assess how well this article fits this publication's readership.`;
}
}
const messages = [
{
role: 'user',
content: userPrompt
}
];
const response = await claudeAPI.sendMessage(messages, {
system: systemPrompt,
max_tokens: 2048,
temperature: 0.3 // Lower temperature for consistent analysis
});
const analysis = claudeAPI.extractJSON(response);
logger.info(`Analysis complete for: ${article.title}`);
return analysis;
} catch (error) {
logger.error('Content analysis error:', error);
throw new Error(`Failed to analyze article: ${error.message}`);
}
}
/**
* Batch analyze multiple articles
* @param {Array<Object>} articles - Articles to analyze
* @returns {Promise<Array<Object>>} Analysis results
*/
async batchAnalyze(articles) {
logger.info(`Batch analyzing ${articles.length} articles`);
const results = [];
for (const article of articles) {
try {
const analysis = await this.analyzeArticle(article);
results.push({
title: article.title,
success: true,
analysis
});
} catch (error) {
logger.error(`Failed to analyze ${article.title}:`, error);
results.push({
title: article.title,
success: false,
error: error.message
});
}
}
return results;
}
/**
* Compare article against publication requirements
* @param {Object} article - Article with content and metadata
* @param {string} publicationId - Publication to compare against
* @returns {Promise<Object>} Comparison results
*/
async compareToPublication(article, publicationId) {
const publication = publicationConfig.getPublicationById(publicationId);
if (!publication) {
throw new Error(`Publication not found: ${publicationId}`);
}
logger.info(`Comparing "${article.title}" to ${publication.name}`);
const analysis = await this.analyzeArticle({
...article,
targetPublication: publicationId
});
// Build comparison report
const comparison = {
publication: {
id: publication.id,
name: publication.name,
type: publication.type
},
article: {
title: article.title,
wordCount: article.wordCount
},
fit: {
overall: analysis.publicationFit?.score || 0,
reasoning: analysis.publicationFit?.reasoning || 'No fit assessment available'
},
requirements: {
wordCount: {
required: publication.requirements?.wordCount || 'Not specified',
actual: article.wordCount,
meets: this._checkWordCount(article.wordCount, publication.requirements?.wordCount)
},
tone: {
preferred: publication.editorial?.tone || [],
detected: analysis.tone.primary,
matches: publication.editorial?.tone?.includes(analysis.tone.primary)
},
audience: {
target: publication.readership?.primary || 'Not specified',
detected: analysis.audience.primary,
matches: publication.readership?.primary === analysis.audience.primary
}
},
recommendations: analysis.publicationFit?.recommendations || [],
analysis
};
return comparison;
}
/**
* Generate improvement suggestions based on analysis
* @param {Object} analysis - Analysis results from analyzeArticle
* @param {string} targetPublicationId - Optional target publication
* @returns {Array<Object>} Prioritized improvement suggestions
*/
generateImprovements(analysis, targetPublicationId = null) {
const improvements = [];
// Check tone alignment
if (targetPublicationId) {
const publication = publicationConfig.getPublicationById(targetPublicationId);
if (publication && publication.editorial) {
const toneMatches = publication.editorial.tone.includes(analysis.tone.primary);
if (!toneMatches) {
improvements.push({
priority: 'high',
category: 'tone',
issue: `Tone mismatch: Article is ${analysis.tone.primary}, but ${publication.name} prefers ${publication.editorial.tone.join(' or ')}`,
suggestion: `Revise to adopt a more ${publication.editorial.tone[0]} tone throughout`
});
}
}
}
// Check audience alignment
if (analysis.audience.confidence < 0.7) {
improvements.push({
priority: 'medium',
category: 'audience',
issue: 'Unclear target audience',
suggestion: 'Clarify who this article is for and adjust language/examples accordingly'
});
}
// Add weaknesses as improvements
if (analysis.weaknesses && analysis.weaknesses.length > 0) {
analysis.weaknesses.forEach((weakness, index) => {
improvements.push({
priority: index === 0 ? 'high' : 'medium',
category: 'content',
issue: weakness,
suggestion: 'Address this weakness in revision'
});
});
}
// Check Tractatus alignment
if (analysis.tractatus && analysis.tractatus.frameworkAlignment < 0.7) {
improvements.push({
priority: 'high',
category: 'framework',
issue: 'Weak Tractatus framework alignment',
suggestion: 'Strengthen connection to Tractatus principles and examples'
});
}
// Publication-specific recommendations
if (analysis.publicationFit && analysis.publicationFit.recommendations) {
analysis.publicationFit.recommendations.forEach(rec => {
improvements.push({
priority: 'high',
category: 'publication-fit',
issue: 'Publication fit improvement',
suggestion: rec
});
});
}
// Sort by priority
const priorityOrder = { high: 0, medium: 1, low: 2 };
improvements.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
return improvements;
}
/**
* Helper: Get publication name by ID
*/
_getPublicationName(publicationId) {
const publication = publicationConfig.getPublicationById(publicationId);
return publication ? publication.name : publicationId;
}
/**
* Helper: Check if word count meets requirements
*/
_checkWordCount(actual, required) {
if (!required) return true;
if (typeof required === 'string') {
// Parse range like "750-1500"
const match = required.match(/(\d+)-(\d+)/);
if (match) {
const min = parseInt(match[1]);
const max = parseInt(match[2]);
return actual >= min && actual <= max;
}
}
return true; // Default to true if can't parse
}
}
// Singleton instance
let instance = null;
module.exports = {
getInstance: () => {
if (!instance) {
instance = new ContentAnalyzerService();
}
return instance;
},
ContentAnalyzerService
};