aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
570 lines (483 loc) • 17.8 kB
text/typescript
/**
* @file review-synthesizer.ts
* @description Intelligent synthesis of multiple review comments and feedback
*
* Consolidates feedback from multiple reviewers (security, testing, requirements, etc.)
* into actionable changes for final artifact generation.
*
* @implements NFR-QUAL-001: 100% review comment traceability
*/
import type { ReviewResult, ReviewComment, ReviewStatus } from './agent-orchestrator.js';
// ============================================================================
// Types and Interfaces
// ============================================================================
export interface ConflictResolution {
conflictType: 'contradictory' | 'overlapping' | 'priority';
involvedReviewers: string[];
description: string;
resolution: string;
reasoning: string;
}
export interface ConsolidatedComment {
id: string;
severity: 'critical' | 'major' | 'minor' | 'info';
section: string;
consolidatedText: string;
sources: Array<{ reviewer: string; originalComment: string }>;
actionItems: string[];
resolved: boolean;
}
export interface SynthesisResult {
reviewCount: number;
approvedCount: number;
conditionalCount: number;
rejectedCount: number;
overallStatus: ReviewStatus;
consolidatedComments: ConsolidatedComment[];
conflicts: ConflictResolution[];
actionPlan: string[];
synthesisQuality: number; // 0-1 score indicating synthesis confidence
}
export interface SynthesisOptions {
prioritizeByRole?: boolean; // Security > Requirements > Testing > Style
allowConflicts?: boolean; // Default: false (raise error on conflicts)
minConsensusThreshold?: number; // 0-1, default: 0.5 (50% agreement required)
}
/**
* Internal type extending ReviewComment with reviewer metadata for consolidation
*/
interface CommentWithReviewer extends ReviewComment {
_reviewer: string;
}
// ============================================================================
// Review Synthesizer Class
// ============================================================================
export class ReviewSynthesizer {
private readonly rolePriorities: Map<string, number> = new Map([
['security-architect', 100],
['requirements-analyst', 90],
['test-architect', 80],
['performance-engineer', 70],
['architecture-designer', 60],
['devops-engineer', 50],
['technical-writer', 40],
['documentation-synthesizer', 30]
]);
/**
* Synthesize multiple review results into consolidated feedback
*/
public synthesize(
reviews: ReviewResult[],
options: SynthesisOptions = {}
): SynthesisResult {
if (reviews.length === 0) {
throw new Error('Cannot synthesize zero reviews');
}
// Count review statuses
const approvedCount = reviews.filter(r => r.status === 'approved').length;
const conditionalCount = reviews.filter(r => r.status === 'conditional').length;
const rejectedCount = reviews.filter(r => r.status === 'rejected').length;
const approvedWithChangesCount = reviews.filter(r => r.status === 'approved-with-changes').length;
// Determine overall status
const overallStatus = this.determineOverallStatus(
reviews,
options.minConsensusThreshold || 0.5
);
// Consolidate all comments
const consolidatedComments = this.consolidateComments(reviews, options);
// Detect conflicts
const conflicts = this.detectConflicts(consolidatedComments, reviews, options);
// Generate action plan
const actionPlan = this.generateActionPlan(consolidatedComments);
// Calculate synthesis quality
const synthesisQuality = this.calculateSynthesisQuality(
reviews,
consolidatedComments,
conflicts
);
return {
reviewCount: reviews.length,
approvedCount: approvedCount + approvedWithChangesCount,
conditionalCount,
rejectedCount,
overallStatus,
consolidatedComments,
conflicts,
actionPlan,
synthesisQuality
};
}
/**
* Determine overall review status based on individual reviews
*/
private determineOverallStatus(
reviews: ReviewResult[],
consensusThreshold: number
): ReviewStatus {
const statusCounts = new Map<ReviewStatus, number>();
for (const review of reviews) {
statusCounts.set(review.status, (statusCounts.get(review.status) || 0) + 1);
}
const totalReviews = reviews.length;
const approvedCount = (statusCounts.get('approved') || 0) + (statusCounts.get('approved-with-changes') || 0);
const rejectedCount = statusCounts.get('rejected') || 0;
// Any rejection means conditional at minimum
if (rejectedCount > 0) {
return 'conditional';
}
// Check if we meet consensus threshold
const approvalRate = approvedCount / totalReviews;
if (approvalRate >= consensusThreshold) {
// If all approved without changes, status is approved
if (statusCounts.get('approved-with-changes') === 0) {
return 'approved';
}
return 'approved-with-changes';
}
return 'conditional';
}
/**
* Consolidate comments from multiple reviewers
*/
private consolidateComments(
reviews: ReviewResult[],
_options: SynthesisOptions
): ConsolidatedComment[] {
const commentGroups = new Map<string, ReviewComment[]>();
// Group comments by section
for (const review of reviews) {
for (const comment of review.comments) {
const section = this.normalizeSection(comment.section);
if (!commentGroups.has(section)) {
commentGroups.set(section, []);
}
commentGroups.get(section)!.push({
...comment,
_reviewer: review.agentType
} as CommentWithReviewer);
}
}
// Consolidate comments within each section
const consolidated: ConsolidatedComment[] = [];
let commentCounter = 0;
for (const [section, comments] of commentGroups.entries()) {
// Group similar comments
const similarGroups = this.groupSimilarComments(comments);
for (const group of similarGroups) {
consolidated.push({
id: `comment-${++commentCounter}`,
severity: this.determineMostSevereSeverity(group),
section,
consolidatedText: this.mergeCommentTexts(group),
sources: group.map(c => ({
reviewer: (c as CommentWithReviewer)._reviewer,
originalComment: c.comment
})),
actionItems: this.extractActionItems(group),
resolved: false
});
}
}
// Sort by severity (critical first)
return consolidated.sort((a, b) => {
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
return severityOrder[a.severity] - severityOrder[b.severity];
});
}
/**
* Normalize section names for grouping
*/
private normalizeSection(section: string): string {
return section
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-');
}
/**
* Group similar comments together
*/
private groupSimilarComments(comments: ReviewComment[]): ReviewComment[][] {
if (comments.length === 0) return [];
if (comments.length === 1) return [comments];
const groups: ReviewComment[][] = [];
const used = new Set<number>();
for (let i = 0; i < comments.length; i++) {
if (used.has(i)) continue;
const group = [comments[i]];
used.add(i);
// Find similar comments
for (let j = i + 1; j < comments.length; j++) {
if (used.has(j)) continue;
if (this.areCommentsSimilar(comments[i], comments[j])) {
group.push(comments[j]);
used.add(j);
}
}
groups.push(group);
}
return groups;
}
/**
* Check if two comments are similar enough to consolidate
*/
private areCommentsSimilar(a: ReviewComment, b: ReviewComment): boolean {
// Simple keyword-based similarity (in production, use Levenshtein distance or NLP)
const aWords = new Set(a.comment.toLowerCase().split(/\s+/));
const bWords = new Set(b.comment.toLowerCase().split(/\s+/));
const intersection = new Set([...aWords].filter(w => bWords.has(w)));
const union = new Set([...aWords, ...bWords]);
const similarity = intersection.size / union.size;
return similarity > 0.4; // 40% word overlap
}
/**
* Determine the most severe severity from a group
*/
private determineMostSevereSeverity(
comments: ReviewComment[]
): 'critical' | 'major' | 'minor' | 'info' {
const severities = comments.map(c => c.severity);
if (severities.includes('critical')) return 'critical';
if (severities.includes('major')) return 'major';
if (severities.includes('minor')) return 'minor';
return 'info';
}
/**
* Merge comment texts from similar comments
*/
private mergeCommentTexts(comments: ReviewComment[]): string {
if (comments.length === 1) {
return comments[0].comment;
}
// Find common theme
const allText = comments.map(c => c.comment).join(' ');
const words = allText.toLowerCase().split(/\s+/);
const wordFreq = new Map<string, number>();
for (const word of words) {
if (word.length > 3) { // Ignore short words
wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
}
}
// Get top keywords
/* const _topKeywords = Array.from(wordFreq.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([word]) => word); */
// Build consolidated text
const reviewerCount = comments.length;
const consolidatedText = `${reviewerCount} reviewers noted: ${comments[0].comment}`;
if (comments.length > 1) {
const additionalComments = comments.slice(1).map(c => c.comment).join('; ');
return `${consolidatedText} Additional feedback: ${additionalComments}`;
}
return consolidatedText;
}
/**
* Extract action items from comments
*/
private extractActionItems(comments: ReviewComment[]): string[] {
const actions: string[] = [];
for (const comment of comments) {
if (comment.suggestedFix) {
actions.push(comment.suggestedFix);
} else {
// Generate action from comment
actions.push(`Address: ${comment.comment}`);
}
}
return Array.from(new Set(actions)); // Deduplicate
}
/**
* Detect conflicts between reviewer comments
*/
private detectConflicts(
consolidatedComments: ConsolidatedComment[],
_reviews: ReviewResult[],
options: SynthesisOptions
): ConflictResolution[] {
const conflicts: ConflictResolution[] = [];
// Check for contradictory comments
for (let i = 0; i < consolidatedComments.length; i++) {
for (let j = i + 1; j < consolidatedComments.length; j++) {
const commentA = consolidatedComments[i];
const commentB = consolidatedComments[j];
// Same section, different actions
if (commentA.section === commentB.section &&
this.areActionsContradictory(commentA.actionItems, commentB.actionItems)) {
const conflict: ConflictResolution = {
conflictType: 'contradictory',
involvedReviewers: [
...commentA.sources.map(s => s.reviewer),
...commentB.sources.map(s => s.reviewer)
],
description: `Contradictory feedback on ${commentA.section}`,
resolution: this.resolveContradiction(commentA, commentB, options),
reasoning: 'Prioritized by reviewer role and severity'
};
conflicts.push(conflict);
}
}
}
return conflicts;
}
/**
* Check if action items contradict each other
*/
private areActionsContradictory(actionsA: string[], actionsB: string[]): boolean {
// Simple keyword-based contradiction detection
const contradictoryPairs = [
['add', 'remove'],
['include', 'exclude'],
['use', "don't use"],
['enable', 'disable']
];
for (const actionA of actionsA) {
for (const actionB of actionsB) {
for (const [wordA, wordB] of contradictoryPairs) {
if (actionA.toLowerCase().includes(wordA) &&
actionB.toLowerCase().includes(wordB)) {
return true;
}
}
}
}
return false;
}
/**
* Resolve contradiction between two comments
*/
private resolveContradiction(
commentA: ConsolidatedComment,
commentB: ConsolidatedComment,
options: SynthesisOptions
): string {
// If prioritizing by role, use role hierarchy
if (options.prioritizeByRole !== false) {
const priorityA = Math.max(...commentA.sources.map(s =>
this.rolePriorities.get(s.reviewer) || 0
));
const priorityB = Math.max(...commentB.sources.map(s =>
this.rolePriorities.get(s.reviewer) || 0
));
if (priorityA > priorityB) {
return `Follow ${commentA.sources[0].reviewer} recommendation: ${commentA.actionItems[0]}`;
} else {
return `Follow ${commentB.sources[0].reviewer} recommendation: ${commentB.actionItems[0]}`;
}
}
// Otherwise, prioritize by severity
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
if (severityOrder[commentA.severity] < severityOrder[commentB.severity]) {
return `Prioritize higher severity: ${commentA.actionItems[0]}`;
} else {
return `Prioritize higher severity: ${commentB.actionItems[0]}`;
}
}
/**
* Generate action plan from consolidated comments
*/
private generateActionPlan(comments: ConsolidatedComment[]): string[] {
const plan: string[] = [];
// Group by severity
const critical = comments.filter(c => c.severity === 'critical');
const major = comments.filter(c => c.severity === 'major');
const minor = comments.filter(c => c.severity === 'minor');
// Critical items first
if (critical.length > 0) {
plan.push('CRITICAL ITEMS (must fix):');
for (const comment of critical) {
plan.push(` - [${comment.section}] ${comment.actionItems[0]}`);
}
}
// Major items
if (major.length > 0) {
plan.push('MAJOR ITEMS (should fix):');
for (const comment of major) {
plan.push(` - [${comment.section}] ${comment.actionItems[0]}`);
}
}
// Minor items
if (minor.length > 0) {
plan.push('MINOR ITEMS (optional):');
for (const comment of minor) {
plan.push(` - [${comment.section}] ${comment.actionItems[0]}`);
}
}
return plan;
}
/**
* Calculate synthesis quality score
*/
private calculateSynthesisQuality(
reviews: ReviewResult[],
consolidatedComments: ConsolidatedComment[],
conflicts: ConflictResolution[]
): number {
let score = 1.0;
// Penalize for rejections
const rejectedCount = reviews.filter(r => r.status === 'rejected').length;
score -= rejectedCount * 0.2;
// Penalize for conflicts
score -= conflicts.length * 0.1;
// Penalize for too many critical comments
const criticalCount = consolidatedComments.filter(c => c.severity === 'critical').length;
score -= criticalCount * 0.05;
// Bonus for high consensus
const approvedCount = reviews.filter(r =>
r.status === 'approved' || r.status === 'approved-with-changes'
).length;
const consensusRate = approvedCount / reviews.length;
score += (consensusRate - 0.5) * 0.2; // Bonus if >50% approval
return Math.max(0, Math.min(1, score)); // Clamp to [0, 1]
}
/**
* Generate synthesis report (markdown format)
*/
public generateReport(synthesis: SynthesisResult): string {
const lines: string[] = [];
lines.push('# Review Synthesis Report\n');
lines.push(`**Generated**: ${new Date().toISOString()}\n`);
lines.push(`**Quality Score**: ${(synthesis.synthesisQuality * 100).toFixed(1)}%\n`);
// Overall Status
lines.push('## Overall Status\n');
lines.push(`- **Status**: ${synthesis.overallStatus.toUpperCase()}`);
lines.push(`- **Reviews**: ${synthesis.reviewCount}`);
lines.push(`- **Approved**: ${synthesis.approvedCount}`);
lines.push(`- **Conditional**: ${synthesis.conditionalCount}`);
lines.push(`- **Rejected**: ${synthesis.rejectedCount}\n`);
// Action Plan
if (synthesis.actionPlan.length > 0) {
lines.push('## Action Plan\n');
lines.push(synthesis.actionPlan.join('\n'));
lines.push('');
}
// Consolidated Comments
if (synthesis.consolidatedComments.length > 0) {
lines.push('## Detailed Feedback\n');
for (const comment of synthesis.consolidatedComments) {
lines.push(`### [${comment.severity.toUpperCase()}] ${comment.section}\n`);
lines.push(`**ID**: ${comment.id}`);
lines.push(`**Comment**: ${comment.consolidatedText}`);
lines.push(`**Sources**: ${comment.sources.map(s => s.reviewer).join(', ')}`);
lines.push(`**Action Items**:`);
for (const action of comment.actionItems) {
lines.push(` - ${action}`);
}
lines.push('');
}
}
// Conflicts
if (synthesis.conflicts.length > 0) {
lines.push('## Conflicts Detected\n');
for (const conflict of synthesis.conflicts) {
lines.push(`### ${conflict.conflictType.toUpperCase()} Conflict\n`);
lines.push(`**Reviewers**: ${conflict.involvedReviewers.join(', ')}`);
lines.push(`**Description**: ${conflict.description}`);
lines.push(`**Resolution**: ${conflict.resolution}`);
lines.push(`**Reasoning**: ${conflict.reasoning}\n`);
}
}
return lines.join('\n');
}
}
export default ReviewSynthesizer;