Skip to main content

ComparisonGenerationService Deep Dive

Overview

The ComparisonGenerationService generates AI-powered comparison pages between pairs of work items. It supports both automatic pair selection (picking the next best un-compared pair) and manual pair specification. The pipeline researches items via web search, generates structured comparison data with dimension-by-dimension scoring, and produces a full markdown article -- all committed to the work's data repository.

Architecture

The comparison system is split into a service layer and a set of pure functions for pair selection, research, and content generation. This design enables thorough unit testing of the core logic without service dependencies.

ComparisonGenerationService (NestJS service)
|
+-- Pair Selection Layer (pure functions)
| +-- selectNextPair()
| +-- findManualPair()
| +-- countRemainingPairs()
| +-- buildPairKey()
|
+-- Research Layer (pure function + injected deps)
| +-- researchPair()
| +-- buildSearchQueries()
|
+-- Generation Layer (pure function + injected deps)
| +-- generateComparison()
| +-- AI structured data (askJson)
| +-- AI markdown article (askText)
| +-- AI extended analysis (askText, optional)
|
+-- Persistence Layer
+-- DataRepository.writeComparison()
+-- DataRepository.writeComparisonMarkdown()
+-- Git add/commit/push

API Reference

Methods

generateNextComparison(workId, userId)

Automatically selects and generates the next comparison pair.

ParameterTypeDescription
workIdstringThe work ID
userIdstringThe requesting user's ID

Returns: Promise<ComparisonResult>

interface ComparisonResult {
status: 'success' | 'skipped' | 'error';
slug?: string;
message: string;
}

generateManualComparison(workId, userId, itemASlug, itemBSlug)

Generates a comparison for two specifically chosen items.

Returns: Promise<ComparisonResult>

getRemainingCount(workId, userId)

Counts how many un-generated comparison pairs remain.

Returns: Promise<number>

listComparisons(workId, userId)

Lists all existing comparisons for a work.

Returns: Promise<ComparisonData[]>

getComparison(workId, userId, slug)

Retrieves a single comparison by slug, including markdown content and optional extended analysis.

Returns: Promise<{ comparison: ComparisonData | null; markdown?: string; extendedAnalysisMarkdown?: string }>

deleteComparison(workId, userId, slug)

Deletes a comparison by slug, updates the comparison state, and pushes the changes.

Returns: Promise<ComparisonResult>

Implementation Details

Pair Selection Algorithm

The selectNextPair() function implements a category-aware, priority-sorted pair selection:

  1. Group items by category -- items are grouped by their primary category
  2. Filter by minimum size -- categories with fewer items than min_items_for_comparison (default: 3) are skipped
  3. Sort by priority -- within each category, items are sorted: featured first, then by order, then alphabetically
  4. Generate all pairs -- all combinatorial pairs within a category are generated
  5. Skip existing -- pairs already in generated_pairs are filtered out
  6. Return first match -- the first un-generated pair is returned

Canonical Pair Keys

Pair keys are order-independent. buildPairKey('vercel', 'netlify') and buildPairKey('netlify', 'vercel') both produce "netlify--vercel". This prevents duplicate comparisons regardless of argument order.

Research Pipeline

The researchPair() function:

  1. Generates up to 4 search queries per pair (e.g., "Vercel vs Netlify", compare Vercel and Netlify)
  2. Executes up to 3 queries via the search facade
  3. Deduplicates results by URL
  4. Extracts content from up to 5 top results
  5. Trims each extraction to 2,000 characters to control token costs
  6. Falls back to search snippets when extraction fails

Comparison Generation

The generateComparison() function makes two (optionally three) AI calls:

  1. Structured data (askJson): Generates title, summary, verdict, verdict winner, and 3-8 scored dimensions
  2. Markdown article (askText): Produces a full article with introduction, feature table, analysis, pros/cons, and verdict
  3. Extended analysis (askText, optional): Deep-dive covering feature breakdowns, use-case analysis, migration considerations, cost analysis, and future outlook

Dimension Scoring

Each comparison dimension includes:

  • Per-item summaries
  • Scores from 1-10 for each item
  • A winner designation (item_a, item_b, or tie)

The overall verdict also designates a winner.

Plugin Settings

Comparison behavior is configurable per-work via the comparison-generator plugin:

SettingDefaultDescription
max_comparisons_modecustomcustom (uses max_comparisons) or unlimited
max_comparisons50Maximum total comparisons to generate
min_items_for_comparison3Minimum items in a category to enable comparisons
ai_provider(default)Override AI provider for comparisons
ai_model(default)Override AI model for comparisons
custom_prompt(none)Additional instructions appended to all prompts
extended_analysisfalseWhether to generate extended analysis markdown

Database Interactions

RepositoryMethodPurpose
WorkRepositoryfindByIdLoad work entity
WorkPluginRepositoryfindByWorkAndPluginLoad comparison plugin settings
DataRepositorygetItems, getConfig, getComparisons, writeComparison, writeComparisonMarkdown, removeComparison, mergeConfigGit-backed data operations

Event System

This service does not emit domain events. Comparison state is tracked in the data repository's config metadata.

Error Handling

ScenarioResult
Work not foundNotFoundException
No pairs available{ status: 'skipped', message: 'No more pairs available' }
Item slugs not found{ status: 'error', message: 'Could not find items...' }
Comparison already exists{ status: 'skipped' } with existing slug
Comparison not found on delete{ status: 'error' }
Search/extraction failuresSilently degraded -- continues with available snippets
Missing item slugsThrows Error during generation

Usage Examples

// Generate the next automatic comparison
const result = await comparisonService.generateNextComparison(workId, userId);
// { status: 'success', slug: 'netlify--vercel', message: 'Generated comparison: ...' }

// Generate a specific comparison
const result = await comparisonService.generateManualComparison(workId, userId, 'react', 'vue');

// Check remaining pairs
const remaining = await comparisonService.getRemainingCount(workId, userId);
// 45

// List all comparisons
const comparisons = await comparisonService.listComparisons(workId, userId);

// Get a comparison with markdown
const detail = await comparisonService.getComparison(workId, userId, 'netlify--vercel');

// Delete a comparison
await comparisonService.deleteComparison(workId, userId, 'netlify--vercel');

Configuration

SettingSourceDescription
Plugin settingsWorkPluginRepositoryPer-work comparison configuration
AI provider/modelPlugin settings or system defaultControls which AI generates comparisons
Search facadeSystem configurationWeb search provider for research
Content extractorSystem configurationWeb content extraction provider