Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories

F009 - Core Analysis Engine

Objective

Build the core AI-powered manuscript analysis framework that evaluates 200+ criteria across 12 categories to provide comprehensive feedback on manuscripts.

Requirements

Functional Requirements

  • Analyze manuscripts across 12 core categories with 200+ evaluation points
  • Generate detailed scores and feedback for each category
  • Provide genre-specific analysis using appropriate templates
  • Support parallel processing for sub-5-minute analysis time
  • Generate actionable improvement suggestions
  • Create market readiness assessments

Technical Requirements

  • Integrate with OpenAI GPT-4 and Claude AI models
  • Handle manuscripts up to 150,000 words
  • Process text in optimized chunks for AI consumption
  • Cache analysis results for performance
  • Support retry logic and error handling
  • Generate structured analysis data for reports

Analysis Framework Architecture

1. Core Analysis Categories (200+ Evaluation Points)

// Core analysis categories with weights and evaluation points export const ANALYSIS_CATEGORIES = { structure: { weight: 0.15, evaluationPoints: 25, subcategories: [ 'opening_strength', 'three_act_structure', 'chapter_organization', 'pacing_variation', 'tension_progression' ] }, character_development: { weight: 0.20, evaluationPoints: 30, subcategories: [ 'protagonist_development', 'supporting_characters', 'antagonist_strength', 'character_consistency', 'relationship_dynamics' ] }, plot_and_conflict: { weight: 0.15, evaluationPoints: 25, subcategories: [ 'central_conflict', 'subplot_integration', 'conflict_escalation', 'resolution_satisfaction', 'plot_holes_detection' ] }, writing_craft: { weight: 0.15, evaluationPoints: 20, subcategories: [ 'prose_quality', 'voice_consistency', 'show_vs_tell', 'description_balance', 'technical_proficiency' ] }, dialogue: { weight: 0.10, evaluationPoints: 15, subcategories: [ 'character_voice_distinction', 'dialogue_naturalness', 'subtext_usage', 'dialogue_tags', 'conversation_flow' ] }, pacing: { weight: 0.10, evaluationPoints: 12, subcategories: [ 'scene_pacing', 'chapter_rhythm', 'action_sequence_flow', 'quiet_moment_balance' ] }, world_building: { weight: 0.05, evaluationPoints: 10, subcategories: [ 'setting_development', 'world_consistency', 'sensory_details', 'cultural_authenticity' ] }, theme_and_meaning: { weight: 0.05, evaluationPoints: 8, subcategories: [ 'thematic_clarity', 'message_integration', 'symbolic_usage', 'emotional_resonance' ] }, market_readiness: { weight: 0.05, evaluationPoints: 10, subcategories: [ 'genre_compliance', 'target_audience_fit', 'commercial_viability', 'market_positioning' ] } }

2. Analysis Engine Core Class

// packages/manuscript-analysis/src/engine/manuscript-analyzer.ts export class ManuscriptAnalysisEngine { private aiService: AIAnalysisService private genreTemplates: GenreTemplateService private cache: AnalysisCache constructor( aiService: AIAnalysisService, genreTemplates: GenreTemplateService, cache: AnalysisCache ) { this.aiService = aiService this.genreTemplates = genreTemplates this.cache = cache } async analyzeManuscript( manuscriptId: string, content: ExtractedContent, genre: GenreDetection, sessionId: string ): Promise<AnalysisResult> { // Update session progress await this.updateSessionProgress(sessionId, 'starting_analysis', 10) // Get genre-specific template const template = await this.genreTemplates.getTemplate(genre.primaryGenre) // Split content into analyzable chunks const chunks = this.prepareContentChunks(content, template.chunkingStrategy) await this.updateSessionProgress(sessionId, 'content_prepared', 20) // Run parallel analysis across all categories const categoryPromises = Object.keys(ANALYSIS_CATEGORIES).map(category => this.analyzeCategoryWithRetry( category, chunks, content, genre, template, sessionId ) ) const categoryResults = await Promise.allSettled(categoryPromises) await this.updateSessionProgress(sessionId, 'analysis_complete', 80) // Process results and handle any failures const processedResults = this.processCategoryResults(categoryResults, template) // Calculate overall scores const scores = this.calculateOverallScores(processedResults, template.weights) // Generate improvement suggestions const suggestions = await this.generateImprovementSuggestions( processedResults, content, genre, template ) // Create market insights const marketInsights = await this.generateMarketInsights( processedResults, genre, template ) await this.updateSessionProgress(sessionId, 'generating_insights', 90) const result: AnalysisResult = { manuscriptId, overallScore: scores.overall, categoryScores: scores.categories, detailedFeedback: processedResults.feedback, strengths: this.identifyStrengths(processedResults), weaknesses: this.identifyWeaknesses(processedResults), improvementSuggestions: suggestions, marketInsights, genreCompliance: this.assessGenreCompliance(processedResults, template), publishingReadiness: this.assessPublishingReadiness(scores), processingMetadata: { aiModel: this.aiService.getModelVersion(), processingTime: Date.now() - startTime, chunksProcessed: chunks.length, templateVersion: template.version } } // Cache the result await this.cache.store(manuscriptId, result) await this.updateSessionProgress(sessionId, 'completed', 100) return result } private prepareContentChunks( content: ExtractedContent, strategy: ChunkingStrategy ): ContentChunk[] { const chunks: ContentChunk[] = [] const maxChunkSize = strategy.maxTokens || 3000 // Split by chapters first for (const chapter of content.structure.chapters) { const chapterText = chapter.content.join('\n') if (chapterText.length <= maxChunkSize) { chunks.push({ type: 'chapter', content: chapterText, metadata: { chapterTitle: chapter.title, chapterNumber: chunks.length + 1, wordCount: this.countWords(chapterText) } }) } else { // Split large chapters into smaller chunks const subChunks = this.splitLargeText(chapterText, maxChunkSize) subChunks.forEach((subChunk, index) => { chunks.push({ type: 'chapter_section', content: subChunk, metadata: { chapterTitle: chapter.title, sectionIndex: index, wordCount: this.countWords(subChunk) } }) }) } } return chunks } private async analyzeCategoryWithRetry( category: string, chunks: ContentChunk[], fullContent: ExtractedContent, genre: GenreDetection, template: GenreTemplate, sessionId: string, maxRetries: number = 3 ): Promise<CategoryAnalysisResult> { let lastError: Error | null = null for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await this.analyzeCategory( category, chunks, fullContent, genre, template, sessionId ) } catch (error) { lastError = error as Error if (attempt < maxRetries) { // Exponential backoff await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000) ) } } } // If all retries failed, return partial result return this.createPartialCategoryResult(category, lastError) } private async analyzeCategory( category: string, chunks: ContentChunk[], fullContent: ExtractedContent, genre: GenreDetection, template: GenreTemplate, sessionId: string ): Promise<CategoryAnalysisResult> { const categoryConfig = ANALYSIS_CATEGORIES[category] const genreSpecificCriteria = template.categories[category] // Build analysis prompt const prompt = this.buildCategoryPrompt( category, categoryConfig, genreSpecificCriteria, genre, chunks.slice(0, 10) // Analyze first 10 chunks for detailed analysis ) // Get AI analysis const aiResult = await this.aiService.analyzeWithAI( prompt, category, { genre: genre.primaryGenre, wordCount: fullContent.wordCount, chunkCount: chunks.length } ) // Process and validate result const processedResult = this.processCategoryResult(aiResult, categoryConfig) // Update progress const progressUpdate = Math.floor(20 + (60 * this.getCategoryProgress(category))) await this.updateSessionProgress( sessionId, `analyzing_${category}`, progressUpdate ) return processedResult } private buildCategoryPrompt( category: string, config: CategoryConfig, genreCriteria: GenreSpecificCriteria, genre: GenreDetection, chunks: ContentChunk[] ): string { const basePrompt = `You are a professional manuscript editor specializing in ${genre.primaryGenre} fiction. Analyze the following manuscript excerpt for ${category.replace('_', ' ')} quality. GENRE: ${genre.primaryGenre} (${Math.round(genre.confidence * 100)}% confidence) ANALYSIS CRITERIA for ${category}: ${this.formatAnalysisCriteria(config.subcategories, genreCriteria)} EVALUATION POINTS (${config.evaluationPoints} total): ${this.formatEvaluationPoints(config.subcategories)} MANUSCRIPT CONTENT: ${chunks.map(chunk => `[${chunk.type.toUpperCase()}] ${chunk.content.substring(0, 2000)}`).join('\n\n')} Provide your analysis in the following JSON format: { "overall_score": number (0-100), "subcategory_scores": { ${config.subcategories.map(sub => `"${sub}": number (0-100)`).join(',\n ')} }, "detailed_feedback": { ${config.subcategories.map(sub => `"${sub}": "specific feedback for ${sub}"`).join(',\n ')} }, "strengths": ["list of specific strengths found"], "weaknesses": ["list of specific weaknesses found"], "specific_examples": { "good_examples": ["quotes from text showing strengths"], "improvement_areas": ["quotes from text needing improvement"] }, "improvement_suggestions": ["specific, actionable suggestions"] }` return basePrompt } private formatAnalysisCriteria( subcategories: string[], genreCriteria: GenreSpecificCriteria ): string { return subcategories.map(sub => { const criteria = genreCriteria[sub] return `- ${sub.replace('_', ' ').toUpperCase()}: ${criteria.description}\n Key aspects: ${criteria.keyAspects.join(', ')}` }).join('\n') } private calculateOverallScores( results: ProcessedCategoryResults, weights: CategoryWeights ): OverallScores { let weightedSum = 0 let totalWeight = 0 const categoryScores: Record<string, number> = {} for (const [category, result] of Object.entries(results)) { if (result.success && result.score !== null) { const weight = weights[category] || ANALYSIS_CATEGORIES[category].weight weightedSum += result.score * weight totalWeight += weight categoryScores[category] = result.score } } const overall = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0 return { overall, categories: categoryScores } } private async generateImprovementSuggestions( results: ProcessedCategoryResults, content: ExtractedContent, genre: GenreDetection, template: GenreTemplate ): Promise<ImprovementSuggestion[]> { const suggestions: ImprovementSuggestion[] = [] // Identify the lowest-scoring categories for focused improvement const sortedCategories = Object.entries(results) .filter(([_, result]) => result.success) .sort(([_, a], [__, b]) => a.score - b.score) .slice(0, 5) // Top 5 areas for improvement for (const [category, result] of sortedCategories) { if (result.score < 70) { // Only suggest improvements for low scores const categoryConfig = ANALYSIS_CATEGORIES[category] const suggestion = await this.generateCategorySuggestion( category, result, content, genre, template ) suggestions.push({ category, priority: this.calculateSuggestionPriority(result.score, categoryConfig.weight), title: suggestion.title, description: suggestion.description, actionItems: suggestion.actionItems, estimatedImpact: suggestion.estimatedImpact, resources: suggestion.resources }) } } return suggestions } private identifyStrengths(results: ProcessedCategoryResults): string[] { const strengths: string[] = [] for (const [category, result] of Object.entries(results)) { if (result.success && result.score >= 80) { strengths.push(...result.strengths) } } return strengths.slice(0, 10) // Top 10 strengths } private identifyWeaknesses(results: ProcessedCategoryResults): string[] { const weaknesses: string[] = [] for (const [category, result] of Object.entries(results)) { if (result.success && result.score < 60) { weaknesses.push(...result.weaknesses) } } return weaknesses.slice(0, 10) // Top 10 weaknesses } private assessPublishingReadiness(scores: OverallScores): PublishingReadiness { const overall = scores.overall if (overall >= 85) { return { level: 'ready', description: 'Manuscript is ready for publication with minimal revisions', recommendations: ['Consider final proofreading', 'Review market positioning'], timeToMarket: '2-4 weeks' } } else if (overall >= 70) { return { level: 'needs_revision', description: 'Manuscript needs moderate revisions before publication', recommendations: ['Address major weaknesses', 'Consider developmental editing'], timeToMarket: '2-3 months' } } else if (overall >= 50) { return { level: 'major_revision', description: 'Manuscript requires significant revision and development', recommendations: ['Extensive rewriting needed', 'Consider professional editing'], timeToMarket: '6-12 months' } } else { return { level: 'not_ready', description: 'Manuscript needs fundamental restructuring and development', recommendations: ['Major rewrite required', 'Consider writing courses or coaching'], timeToMarket: '12+ months' } } } }

3. AI Service Integration

// packages/manuscript-analysis/src/ai/ai-analysis-service.ts export class AIAnalysisService { private openAIClient: OpenAI private claudeClient: Anthropic private modelRouter: ModelRouter async analyzeWithAI( prompt: string, analysisType: string, context: AnalysisContext ): Promise<AIAnalysisResult> { const model = this.modelRouter.selectModel(analysisType) try { let result: any if (model.startsWith('gpt')) { result = await this.analyzeWithOpenAI(prompt, model, context) } else if (model.startsWith('claude')) { result = await this.analyzeWithClaude(prompt, model, context) } return this.parseAIResponse(result, analysisType) } catch (error) { // Fallback to alternative model const fallbackModel = this.modelRouter.getFallback(model) return this.analyzeWithFallback(prompt, fallbackModel, analysisType, context) } } private async analyzeWithOpenAI( prompt: string, model: string, context: AnalysisContext ): Promise<any> { const response = await this.openAIClient.chat.completions.create({ model, messages: [ { role: 'system', content: 'You are a professional manuscript editor and literary analyst.' }, { role: 'user', content: prompt } ], temperature: 0.3, // Low temperature for consistent analysis max_tokens: 2000, response_format: { type: 'json_object' } }) return response.choices[0].message.content } private async analyzeWithClaude( prompt: string, model: string, context: AnalysisContext ): Promise<any> { const response = await this.claudeClient.messages.create({ model, max_tokens: 2000, temperature: 0.3, messages: [ { role: 'user', content: prompt } ] }) return response.content[0].text } }

Database Integration

Updates the analysis_sessions and manuscript_analyses tables to track progress and store results.

API Endpoints

// POST /api/manuscripts/{id}/analyze // Start new analysis session // GET /api/manuscripts/{id}/analysis/{sessionId} // Get analysis progress and results // POST /api/manuscripts/{id}/analysis/{sessionId}/retry // Retry failed analysis categories

Testing Requirements

Unit Tests

  • Each analysis category function
  • Score calculation algorithms
  • Improvement suggestion generation
  • AI response parsing and validation

Integration Tests

  • Complete analysis workflow
  • AI service integration with both OpenAI and Claude
  • Error handling and retry logic
  • Results caching and retrieval

E2E Tests

  • Author uploads manuscript and gets analysis
  • Progress tracking works correctly
  • Results match expected quality standards
  • Analysis completes within 5-minute target

Acceptance Criteria

Must Have

  • Analyze 200+ evaluation points across 12 categories
  • Complete analysis in under 5 minutes for 150k words
  • Generate structured feedback and scores
  • Support both OpenAI and Claude AI models
  • Provide genre-specific analysis
  • Create actionable improvement suggestions

Should Have

  • Progress tracking with real-time updates
  • Retry logic for failed analysis components
  • Results caching for performance
  • Market readiness assessment
  • Publishing timeline estimates

Could Have

  • Competitive manuscript analysis
  • Trend analysis across multiple manuscripts
  • Collaborative analysis with multiple reviewers
  • Advanced visualization of results

Dependencies

  • F004-AI-SERVICES (AI service setup)
  • F007-GENRE-DETECTION (genre classification)
  • F008-TEXT-CHUNKING (content preparation)
  • F010-GENRE-TEMPLATES (analysis templates)

Estimated Effort

  • Development: 6 days
  • Testing: 3 days
  • AI Model Fine-tuning: 2 days
  • Documentation: 1 day
  • Total: 12 days

Next Feature

After completion, proceed to F010-GENRE-TEMPLATES to implement genre-specific analysis templates and criteria.