adpa-enterprise-framework-automation
Version:
Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe
404 lines • 16.9 kB
JavaScript
import { ReviewService } from './ReviewService.js';
import { DocumentGenerator } from '../modules/documentGenerator/DocumentGenerator.js';
import { logger } from '../utils/logger.js';
import path from 'path';
export class DocumentReviewIntegration {
reviewService;
constructor() {
this.reviewService = new ReviewService();
}
/**
* Generate documents and automatically create review requests
*/
async generateDocumentsWithReview(options) {
logger.info(`Starting document generation with review for project: ${options.projectName}`);
const result = {
generationResult: {
success: false,
successCount: 0,
failureCount: 0,
skippedCount: 0,
generatedFiles: [],
errors: [],
duration: 0,
message: ''
},
reviewsCreated: [],
errors: [],
summary: {
documentsGenerated: 0,
reviewsCreated: 0,
documentsSubmittedForReview: 0,
documentsSkippedReview: 0
}
};
try {
// Step 1: Generate documents
const generator = new DocumentGenerator(options.context);
let generationResult;
if (options.generateAll) {
generationResult = await generator.generateAll();
}
else if (options.documentKeys?.length) {
// Generate specific documents
generationResult = {
success: true,
successCount: 0,
failureCount: 0,
skippedCount: 0,
generatedFiles: [],
errors: [],
duration: 0,
message: ''
};
const startTime = Date.now();
for (const documentKey of options.documentKeys) {
const success = await generator.generateOne(documentKey);
if (success) {
generationResult.successCount++;
generationResult.generatedFiles.push(`${documentKey}.md`);
}
else {
generationResult.failureCount++;
generationResult.errors.push({ task: documentKey, error: 'Generation failed' });
}
}
generationResult.duration = Date.now() - startTime;
generationResult.success = generationResult.successCount > 0;
generationResult.message = `Generated ${generationResult.successCount} documents`;
}
else {
throw new Error('Either generateAll must be true or documentKeys must be provided');
}
result.generationResult = generationResult;
result.summary.documentsGenerated = generationResult.successCount;
// Step 2: Create review requests for generated documents (if enabled)
if (options.enableReview && generationResult.success) {
await this.createReviewsForGeneratedDocuments(generationResult, options, result);
}
// Step 3: Send notifications if configured
if (options.notifyOnCompletion) {
await this.sendCompletionNotifications(options, result);
}
logger.info(`Document generation with review completed. Generated: ${result.summary.documentsGenerated}, Reviews: ${result.summary.reviewsCreated}`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Error in document generation with review:', error);
result.errors.push(errorMessage);
result.generationResult.success = false;
result.generationResult.message = `Failed: ${errorMessage}`;
}
return result;
}
/**
* Create review requests for successfully generated documents
*/
async createReviewsForGeneratedDocuments(generationResult, options, result) {
logger.info(`Creating reviews for ${generationResult.generatedFiles.length} generated documents`);
for (const filePath of generationResult.generatedFiles) {
try {
const documentInfo = this.extractDocumentInfo(filePath);
// Skip if document type should not be reviewed
if (!this.shouldCreateReview(documentInfo.type, options)) {
result.summary.documentsSkippedReview++;
continue;
}
const reviewRequest = {
documentId: this.generateDocumentId(options.projectId, documentInfo.name),
documentName: documentInfo.name,
documentType: documentInfo.type,
documentPath: path.join(process.cwd(), 'generated-documents', filePath),
projectId: options.projectId,
priority: options.reviewPriority,
requiredRoles: options.requiredRoles,
specificReviewers: options.specificReviewers,
workflowId: options.workflowId,
metadata: {
generatedAt: new Date().toISOString(),
projectName: options.projectName,
generationContext: options.context,
autoGenerated: true
}
};
const review = await this.reviewService.createReview(reviewRequest);
result.reviewsCreated.push(review);
result.summary.reviewsCreated++;
if (options.autoSubmitForReview) {
result.summary.documentsSubmittedForReview++;
}
logger.info(`Review created for document: ${documentInfo.name} (ID: ${review.id})`);
}
catch (error) {
const errorMessage = `Failed to create review for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`;
logger.error(errorMessage);
result.errors.push(errorMessage);
}
}
}
/**
* Extract document information from file path
*/
extractDocumentInfo(filePath) {
const pathParts = filePath.split('/');
const fileName = pathParts[pathParts.length - 1];
const category = pathParts.length > 1 ? pathParts[pathParts.length - 2] : 'general';
// Remove file extension
const name = fileName.replace(/\.[^/.]+$/, '');
// Determine document type based on category and name
let type = 'general';
if (category.includes('pmbok')) {
type = 'pmbok_document';
}
else if (category.includes('babok')) {
type = 'babok_document';
}
else if (category.includes('dmbok')) {
type = 'dmbok_document';
}
else if (category.includes('technical')) {
type = 'technical_document';
}
else if (category.includes('planning')) {
type = 'planning_document';
}
else if (category.includes('management')) {
type = 'management_plan';
}
else if (category.includes('stakeholder')) {
type = 'stakeholder_document';
}
else if (category.includes('quality')) {
type = 'quality_document';
}
else if (category.includes('risk')) {
type = 'risk_document';
}
return {
name: this.formatDocumentName(name),
type,
category
};
}
/**
* Format document name for display
*/
formatDocumentName(name) {
return name
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Generate unique document ID
*/
generateDocumentId(projectId, documentName) {
const timestamp = Date.now();
const sanitizedName = documentName.toLowerCase().replace(/[^a-z0-9]/g, '-');
return `${projectId}-${sanitizedName}-${timestamp}`;
}
/**
* Determine if a review should be created for a document type
*/
shouldCreateReview(documentType, options) {
// Skip review for certain document types if configured
const skipReviewTypes = [
'index',
'readme',
'summary'
];
if (skipReviewTypes.some(skipType => documentType.toLowerCase().includes(skipType))) {
return false;
}
// Always create reviews for critical document types
const criticalTypes = [
'pmbok_document',
'babok_document',
'technical_document',
'management_plan'
];
if (criticalTypes.includes(documentType)) {
return true;
}
// Default to creating reviews for all other types
return true;
}
/**
* Send completion notifications
*/
async sendCompletionNotifications(options, result) {
try {
logger.info(`Sending completion notifications for project: ${options.projectName}`);
// This would integrate with a notification service
const notificationData = {
projectId: options.projectId,
projectName: options.projectName,
summary: result.summary,
recipients: options.notificationRecipients || [],
generationSuccess: result.generationResult.success,
reviewsCreated: result.reviewsCreated.length,
errors: result.errors
};
// Placeholder for notification service integration
logger.info('Notification data prepared:', notificationData);
}
catch (error) {
logger.error('Error sending completion notifications:', error);
}
}
/**
* Get review status for a project's documents
*/
async getProjectReviewStatus(projectId) {
try {
const { reviews } = await this.reviewService.searchReviews({
projectId,
limit: 1000,
offset: 0
});
const totalDocuments = reviews.length;
const pendingReviews = reviews.filter(r => ['pending_assignment', 'assigned', 'in_review'].includes(r.status)).length;
const completedReviews = reviews.filter(r => ['approved', 'rejected', 'completed'].includes(r.status)).length;
const approvedDocuments = reviews.filter(r => r.status === 'approved').length;
const rejectedDocuments = reviews.filter(r => r.status === 'rejected').length;
let overallStatus = 'pending';
if (completedReviews === totalDocuments && totalDocuments > 0) {
overallStatus = 'completed';
}
else if (completedReviews > 0) {
overallStatus = 'mixed';
}
else if (pendingReviews > 0) {
overallStatus = 'in_progress';
}
return {
totalDocuments,
pendingReviews,
completedReviews,
approvedDocuments,
rejectedDocuments,
overallStatus
};
}
catch (error) {
logger.error('Error getting project review status:', error);
throw error;
}
}
/**
* Regenerate document after review feedback
*/
async regenerateDocumentWithFeedback(reviewId, documentKey, context) {
try {
logger.info(`Regenerating document with feedback for review: ${reviewId}`);
// Get the review to access feedback
const review = await this.reviewService.getReview(reviewId);
if (!review) {
throw new Error('Review not found');
}
// Extract feedback for context enhancement
const feedbackContext = this.extractFeedbackContext(review);
const enhancedContext = `${context}\n\nFeedback to address:\n${feedbackContext}`;
// Regenerate the document
const generator = new DocumentGenerator(enhancedContext);
const success = await generator.generateOne(documentKey);
if (!success) {
return { success: false, error: 'Document regeneration failed' };
}
// Create a new review for the regenerated document
const newReviewRequest = {
documentId: `${review.documentId}-revision-${Date.now()}`,
documentName: `${review.documentName} (Revised)`,
documentType: review.documentType,
documentPath: review.documentPath,
projectId: review.projectId,
priority: review.priority,
metadata: {
...review.metadata,
originalReviewId: reviewId,
regeneratedAt: new Date().toISOString(),
feedbackAddressed: true
}
};
const newReview = await this.reviewService.createReview(newReviewRequest);
// Update original review status
await this.reviewService.updateReviewStatus({
reviewId,
status: 'completed',
comments: `Document regenerated. New review created: ${newReview.id}`
});
logger.info(`Document regenerated successfully. New review: ${newReview.id}`);
return { success: true, newReviewId: newReview.id };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Error regenerating document with feedback:', error);
return { success: false, error: errorMessage };
}
}
/**
* Extract feedback context for document regeneration
*/
extractFeedbackContext(review) {
const feedbackItems = [];
review.reviewRounds.forEach(round => {
round.feedback.forEach(feedback => {
if (feedback.status === 'open' && feedback.severity !== 'info') {
let item = `- ${feedback.title}: ${feedback.description}`;
if (feedback.suggestion) {
item += ` Suggestion: ${feedback.suggestion}`;
}
feedbackItems.push(item);
}
});
});
return feedbackItems.join('\n');
}
/**
* Bulk approve documents after successful review
*/
async bulkApproveDocuments(projectId, documentIds) {
const result = {
approved: [],
failed: [],
errors: []
};
try {
for (const documentId of documentIds) {
try {
// Find review by document ID
const { reviews } = await this.reviewService.searchReviews({
projectId,
limit: 1,
offset: 0
});
const review = reviews.find(r => r.documentId === documentId);
if (!review) {
result.failed.push(documentId);
result.errors.push(`Review not found for document: ${documentId}`);
continue;
}
if (review.status !== 'approved') {
await this.reviewService.updateReviewStatus({
reviewId: review.id,
status: 'approved',
comments: 'Bulk approved'
});
}
result.approved.push(documentId);
}
catch (error) {
result.failed.push(documentId);
result.errors.push(`Failed to approve ${documentId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
logger.info(`Bulk approval completed. Approved: ${result.approved.length}, Failed: ${result.failed.length}`);
}
catch (error) {
logger.error('Error in bulk approve documents:', error);
result.errors.push(error instanceof Error ? error.message : 'Unknown error');
}
return result;
}
}
//# sourceMappingURL=DocumentReviewIntegration.js.map