UNPKG

sfcoe-ailabs

Version:

AI-powered code review tool with static analysis integration for comprehensive code quality assessment.

1,233 lines 59.6 kB
// Import necessary modules
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as process from 'node:process';
import { fileURLToPath, URL } from 'node:url';
import { Command, Flags } from '@oclif/core';
import * as fse from 'fs-extra';
// Import custom modules
import { GitHelper, GitProviderFactory, } from '../../common/gitProvider/index.js';
import { SCATProviderFactory, SCATProviderType, } from '../../common/contextProviders/scat/index.js';
import { AIProviderFactory, } from '../../common/aiProvider/index.js';
import { Logger, Metrics, ObservabilityInitializer, } from '../../common/observability/index.js';
import { FileUtils } from '../../common/utils/fileUtils.js';
import { DocuSignRuleUtils } from '../../common/utils/index.js';
import { COMMAND_DEFAULTS, REVIEW_COMMAND_HINTS_DEFAULTS, AI_PROVIDERS, TEST_PATTERNS, EXCLUDED_DIRECTORIES, DOCUSIGN_SPECIFIC_ISSUES, SUPPORTED_SALESFORCE_EXTENSIONS, SUPPORTED_FILE_EXTENSIONS, } from '../../common/utils/constants.js';
import { MESSAGES } from '../../messages/index.js';
const currentFilename = fileURLToPath(import.meta.url);
const currentDirname = path.dirname(currentFilename);
/**
 * @file index.ts
 *
 * AI-powered Pull Request review command that performs comprehensive static code analysis
 * using a hybrid approach combining Salesforce Code Analyzer (SCA) for Salesforce-specific files
 * and PMD for general file types, then leverages AI providers to generate intelligent code reviews.
 *
 * This command:
 * 1. Analyzes code diffs between two Git references
 * 2. Runs static analysis tools (SCA + PMD) to detect code quality issues
 * 3. Uses AI providers (OpenAI, Anthropic) to generate contextual review comments
 * 4. Identifies Docusign-specific patterns and best practices
 * 5. Posts comprehensive review comments to Pull Requests or outputs locally
 *
 * @author SF COE Team
 * @since 0.1.0
 */
export default class AIPRReview extends Command {
    static summary = MESSAGES['pr-review'].summary;
    static description = MESSAGES['pr-review'].description;
    static examples = MESSAGES['pr-review'].examples;
    /**
     * Command line flags configuration for the AIPRReview command.
     * Defines all available command line options including their types, defaults, and validation
     */
    static flags = {
        'repo-dir': Flags.directory({
            summary: MESSAGES['pr-review'].flags.repoDir,
            char: 'r',
            default: process.cwd(),
            exists: true,
            required: false,
        }),
        from: Flags.string({
            summary: MESSAGES['pr-review'].flags.from,
            char: 'f',
            default: COMMAND_DEFAULTS.GIT_FROM_REF,
            required: false,
        }),
        to: Flags.string({
            summary: MESSAGES['pr-review'].flags.to,
            char: 't',
            default: COMMAND_DEFAULTS.GIT_TO_REF,
            required: false,
        }),
        'pull-request-id': Flags.string({
            summary: MESSAGES['pr-review'].flags.pullRequestId,
            char: 'p',
            required: false,
        }),
        'sf-config-file': Flags.file({
            summary: MESSAGES['pr-review'].flags.sfConfigFile,
            char: 's',
            exists: true,
            required: false,
        }),
        'pmd-config-file': Flags.file({
            summary: MESSAGES['pr-review'].flags.pmdConfigFile,
            char: 'x',
            exists: true,
            required: false,
        }),
        'ai-provider': Flags.option({
            summary: MESSAGES['pr-review'].flags.aiProvider,
            char: 'a',
            required: true,
            multiple: false,
            default: COMMAND_DEFAULTS.AI_PROVIDER,
            options: [
                AI_PROVIDERS.ANTHROPIC,
                AI_PROVIDERS.AZURE_OPENAI,
                AI_PROVIDERS.OPENAI,
            ],
        })(),
        'ai-token': Flags.string({
            summary: MESSAGES['pr-review'].flags.aiToken,
            char: 'n',
            required: true,
        }),
        'ai-model': Flags.string({
            summary: MESSAGES['pr-review'].flags.aiModel,
            char: 'm',
            default: COMMAND_DEFAULTS.AI_MODEL,
            required: true,
        }),
        'ai-api-endpoint': Flags.string({
            summary: MESSAGES['pr-review'].flags.aiApiEndpoint,
            char: 'e',
            default: COMMAND_DEFAULTS.AI_API_ENDPOINT,
            required: false,
        }),
        'ai-api-version': Flags.string({
            summary: MESSAGES['pr-review'].flags.aiApiVersion,
            char: 'v',
            default: COMMAND_DEFAULTS.AI_API_VERSION,
            required: false,
        }),
        'git-provider': Flags.option({
            summary: MESSAGES['pr-review'].flags.gitProvider,
            char: 'g',
            required: false,
            multiple: false,
            options: [COMMAND_DEFAULTS.GIT_PROVIDER],
        })(),
        'git-token': Flags.string({
            summary: MESSAGES['pr-review'].flags.gitToken,
            char: 'k',
            required: false,
        }),
        'git-owner': Flags.string({
            summary: MESSAGES['pr-review'].flags.gitOwner,
            char: 'w',
            required: false,
        }),
        'git-repo': Flags.string({
            summary: MESSAGES['pr-review'].flags.gitRepo,
            char: 'o',
            required: false,
        }),
    };
    /**
     * Initialize the AIProvider and GitProvider based on command flags
     *
     * @param flags Command line flags containing provider configuration
     * @returns Object containing initialized AIProvider and optional GitProvider instances
     * @throws Error if AI provider initialization fails
     * @example
     * ```typescript
     * const { aiProvider, gitProvider } = AIPRReview.initializeProviders(flags);
     * ```
     */
    static initializeProviders(flags) {
        const aiProvider = AIProviderFactory.getInstance(flags['ai-provider'], flags['ai-token'], flags['ai-model'], flags['ai-api-endpoint'], flags['ai-api-version']);
        let gitProvider;
        if (flags['git-provider'] &&
            flags['git-token'] &&
            flags['git-owner'] &&
            flags['git-repo']) {
            gitProvider = GitProviderFactory.getInstance(flags['git-provider'], flags['git-token'], flags['git-owner'], flags['git-repo']);
        }
        return { aiProvider, gitProvider };
    }
    /**
     * Get the commit SHA for PR comments with fallback mechanism
     *
     * @param repoDir Path to the Git repository directory
     * @param flags Command line flags containing the 'to' commit reference
     * @returns Promise resolving to the commit SHA for the PR commit
     * @throws Error if unable to determine any valid commit SHA
     * @example
     * ```typescript
     * const commitSha = await AIPRReview.getCommitSha('/path/to/repo', flags);
     * ```
     */
    static async getCommitSha(repoDir, flags) {
        let commitSHA = '';
        try {
            commitSHA = await GitHelper.verifyCommitSha(repoDir, flags.to);
        }
        catch {
            Logger.warn(`Failed to get commit SHA from ${flags.to}, trying to get latest commit from HEAD.`);
            try {
                commitSHA = await GitHelper.verifyCommitSha(repoDir, COMMAND_DEFAULTS.GIT_TO_REF);
            }
            catch {
                Logger.error('Failed to get any valid commit SHA');
            }
        }
        return commitSHA;
    }
    /**
     * Create a file map from SARIF data by parsing runs and results
     *
     * @param sarifData Parsed SARIF data from PMD or SCA analysis
     * @returns Map of normalized file paths to review hints with rule violations
     * @example
     * ```typescript
     * const fileMap = AIPRReview.createFileMap(sarifData);
     * console.log(`Found issues in ${fileMap.size} files`);
     * ```
     */
    static createFileMap(sarifData) {
        const fileMap = new Map();
        sarifData.runs.forEach((run) => {
            run.results?.forEach((result) => {
                if (!result.ruleId || !result.message?.text || !result.locations)
                    return;
                result.locations.forEach((location) => {
                    if (!location.physicalLocation?.artifactLocation?.uri ||
                        !location.physicalLocation?.region)
                        return;
                    const filePathBase = location.physicalLocation.artifactLocation.uriBaseId ?? '';
                    let filePath = location.physicalLocation.artifactLocation.uri;
                    // Handle file:// URI format
                    if (filePath.startsWith('file:')) {
                        try {
                            // Convert URI to local path
                            const url = new URL(filePath);
                            filePath = url.pathname;
                        }
                        catch {
                            // If URL parsing fails, try simple string replacement
                            filePath = filePath.replace(/^file:\/?\/?/, '');
                        }
                    }
                    // Join with base path if provided
                    if (filePathBase) {
                        filePath = path.join(filePathBase, filePath);
                    }
                    // Remove any duplicate path segments that might have been created
                    filePath = path.normalize(filePath);
                    const region = location.physicalLocation.region;
                    const hint = {
                        ruleId: result.ruleId ?? REVIEW_COMMAND_HINTS_DEFAULTS.RULE_ID,
                        message: result.message.text ?? REVIEW_COMMAND_HINTS_DEFAULTS.MESSAGE,
                        region,
                        className: filePath.split('/').pop() ?? filePath, // Use file name as className
                    };
                    if (!fileMap.has(filePath)) {
                        fileMap.set(filePath, [hint]);
                    }
                    else {
                        fileMap.get(filePath).push(hint);
                    }
                });
            });
        });
        return fileMap;
    }
    /**
     * Main command execution method that orchestrates the entire PR review workflow
     *
     * @returns Promise that resolves when the command execution is complete
     * @throws Error if any critical step in the workflow fails
     * @example
     * ```typescript
     * const diffCommand = new AIPRReview();
     * await diffCommand.run();
     * ```
     */
    async run() {
        // Obtain process start date/time
        const startTime = new Date();
        // Parse command line flags
        const flags = (await this.parse(AIPRReview)).flags;
        // Initialize observability for Datadog logging and metrics
        ObservabilityInitializer.initialize(flags);
        // Initialise the
        const { aiProvider, gitProvider } = AIPRReview.initializeProviders(flags);
        // Create the Git repository directory with diff files
        const repoDir = flags['repo-dir'];
        const diffDir = await GitHelper.createDirectoryWithDiff(repoDir, flags.from, flags.to);
        Logger.debug(`Starting hybrid SCA+PMD code analysis workflow on: \n repoDir = ${repoDir} \n diffDir = ${diffDir}`);
        // Get changed line ranges for filtering vulnerabilities to only changed lines
        const changedLinesRanges = await GitHelper.getChangedLineRanges(repoDir, flags.from, flags.to);
        Logger.debug(`Changed line ranges found for ${changedLinesRanges.size} files`, Array.from(changedLinesRanges.entries()));
        // Run hybrid scan: SCA for Salesforce files, PMD for others
        const combinedFileMap = await this.runHybridAnalysis(diffDir, flags);
        Logger.debug(`Combined analysis found issues in ${combinedFileMap.size} files`, JSON.stringify(combinedFileMap));
        // Get the commit SHA for PR comments
        const commitSha = await AIPRReview.getCommitSha(repoDir, flags);
        // If no commit SHA is found, perform completion
        if (!commitSha) {
            await this.performCompletion(diffDir, startTime);
            return;
        }
        // Process combined results and generate AI-powered review
        if (combinedFileMap.size > 0) {
            await this.processPmdResults(combinedFileMap, diffDir, aiProvider, flags, commitSha, gitProvider, changedLinesRanges);
        }
        else {
            Logger.debug('No issues found in either SCA or PMD analysis, so will delete existing bot comments if any');
            // Handle the case where no violations are found - delete existing comments directly if present
            if (flags['pull-request-id'] && gitProvider) {
                await gitProvider.deleteBotComment(flags['pull-request-id'], true // General comment
                );
            }
        }
        // Perform completion since all operations are completed
        await this.performCompletion(diffDir, startTime);
    }
    /**
     * Perform cleanup and completion tasks after the review process
     *
     * @param diffDir Directory containing the diff files
     * @param startTime Start time of the process
     */
    async performCompletion(diffDir, startTime) {
        // Delete the temporary diff directory if it exists
        if (fs.existsSync(diffDir)) {
            fs.rmdirSync(diffDir, { recursive: true });
        }
        // Set process duration runtime metric
        const processDuration = (new Date().getTime() - startTime.getTime()) / 1000;
        Metrics.gauge('ai_review.duration', processDuration);
        Metrics.gauge('ai_review.process_completed_successfully', 1);
        Logger.debug(`Process started at ${startTime.toISOString()} completed successfully within ${processDuration} seconds`);
        // Close metrics client so that all metrics are flushed to DD
        Metrics.close();
    }
    /**
     * Process PMD/SCA results through AI analysis and generate comprehensive review comments
     *
     * @param fileMap Map of file paths to review hints from static analysis
     * @param diffDir Directory containing the diff files to analyze
     * @param aiProvider AI provider instance for code review generation
     * @param flags Command line flags containing configuration options
     * @param commitSha Git commit SHA for PR comment posting
     * @param gitProvider Optional git provider for posting PR comments
     * @param changedLinesRanges Map of file paths to changed line ranges for filtering
     * @returns Promise that resolves when processing is complete
     * @throws Error if file reading or AI analysis fails
     */
    async processPmdResults(fileMap, diffDir, aiProvider, flags, commitSha, gitProvider, changedLinesRanges) {
        Logger.debug(`Processing PMD results for AI review with ${fileMap.size} files`);
        const fileContexts = await this.readAndProcessFiles(fileMap, diffDir);
        // Create comprehensive code snippet for AI analysis
        const comprehensiveCodeSnippet = this.buildCodeSnippet(fileContexts);
        // Flatten all hints from file contexts for AI analysis
        const allHintsForAI = [];
        for (const context of fileContexts) {
            allHintsForAI.push(...context.hints);
        }
        // Get AI Recommendations for the comprehensive code snippet
        const aiResponse = await aiProvider.reviewCode(comprehensiveCodeSnippet, allHintsForAI);
        Logger.debug(`Received AI Provider response: ${aiResponse.comments.length} comments for AI-assisted review`, {
            aiResponseCommentsCount: aiResponse.comments?.length || 0,
            aiResponseComments: aiResponse.comments,
        });
        Metrics.gauge('ai_review.ai_comments_count', aiResponse.comments?.length || 0);
        // Build comprehensive PMD review message
        const comprehensiveMessage = this.buildReviewComment(aiResponse, fileContexts, changedLinesRanges);
        // Post the comprehensive review comment to PR or output locally
        await this.postPRComment(comprehensiveMessage, flags, commitSha, gitProvider);
    }
    /**
     * Read and process files from the file map, handling path resolution and duplicate prevention
     *
     * @param fileMap Map of file paths to review hints from static analysis
     * @param diffDir Directory containing the diff files to read
     * @returns Promise resolving to object containing file contexts with hints
     * @throws Error if file reading operations fail critically
     * @example
     * ```typescript
     * const fileContexts = await this.readAndProcessFiles(fileMap, diffDir);
     * ```
     */
    async readAndProcessFiles(fileMap, diffDir) {
        const fileContexts = [];
        const processedFilePaths = new Set();
        // Read all files and collect hints
        const fileReadPromises = Array.from(fileMap.entries()).map(async ([filePath, hints]) => {
            // Normalize the file path to prevent duplicates
            const normalizedPath = FileUtils.normalizePath(filePath);
            const shortFileName = FileUtils.getFileName(normalizedPath);
            // Skip if we've already processed this file
            if (processedFilePaths.has(shortFileName)) {
                return null;
            }
            processedFilePaths.add(shortFileName);
            return this.tryReadFile(normalizedPath, diffDir, hints);
        });
        const fileReadResults = await Promise.all(fileReadPromises);
        const fileNames = [];
        for (const result of fileReadResults) {
            if (result) {
                fileNames.push(result.file.split('/').pop());
                fileContexts.push(result);
            }
        }
        Logger.debug(`Files to process for AI-review: \n ${fileNames.join(',')}`, fileNames);
        return fileContexts;
    }
    /**
     * Try to read a file from multiple possible paths with fallback strategies
     *
     * @param filePath Original file path from static analysis
     * @param diffDir Directory containing diff files
     * @param hints Review hints associated with this file
     * @returns Promise resolving to FileContext object if successful, null if all attempts fail
     * @example
     * ```typescript
     * const fileContext = await this.tryReadFile('src/test.js', diffDir, hints);
     * ```
     */
    async tryReadFile(filePath, diffDir, hints) {
        // Try multiple path resolution strategies
        const possiblePaths = [
            path.resolve(diffDir, filePath),
            path.resolve(filePath),
            filePath.startsWith('/') ? filePath : path.resolve(diffDir, filePath),
        ];
        // Remove duplicates
        const uniquePaths = [...new Set(possiblePaths)];
        const readFileAttempts = uniquePaths.map((fullFilePath) => {
            try {
                const fileContent = fs.readFileSync(fullFilePath, 'utf-8');
                return { file: filePath, content: fileContent, hints, success: true };
            }
            catch (error) {
                Logger.debug(`Failed to read ${fullFilePath}, trying next path.`, error);
                return { file: filePath, content: '', hints, success: false };
            }
        });
        const results = await Promise.all(readFileAttempts);
        const successfulResult = results.find((result) => result.success);
        if (successfulResult) {
            return {
                file: successfulResult.file,
                content: successfulResult.content,
                hints: successfulResult.hints,
            };
        }
        Logger.warn(`Failed to read file ${filePath} from any of the attempted paths:`, uniquePaths);
        return null;
    }
    /**
     * Separate hints into those that occur on changed lines and those that don't, grouped by file
     *
     * @param fileContexts Array of file contexts containing file information and hints
     * @param changedLinesRanges Map of file paths to changed line ranges
     * @returns Object containing changedLinesHintsPerFile and remainingHintsPerFile maps
     * @example
     * ```typescript
     * const { changedLinesHintsPerFile, remainingHintsPerFile } = this.separateHintsByChangedLines(fileContexts, changedLinesRanges);
     * ```
     */
    separateHintsByChangedLines(fileContexts, changedLinesRanges) {
        const changedLinesHintsPerFile = new Map();
        const remainingHintsPerFile = new Map();
        // If no changed line ranges, all hints are remaining
        if (!changedLinesRanges || changedLinesRanges.size === 0) {
            for (const { file, hints } of fileContexts) {
                const shortFileName = FileUtils.getFileName(file);
                if (hints.length > 0) {
                    remainingHintsPerFile.set(shortFileName, [...hints]);
                }
            }
            return { changedLinesHintsPerFile, remainingHintsPerFile };
        }
        // Create a map of file names to their contexts for quick lookup
        const fileContextMap = new Map();
        for (const context of fileContexts) {
            const shortFileName = FileUtils.getFileName(context.file);
            fileContextMap.set(shortFileName, context);
        }
        // Process hints from all file contexts and separate them
        for (const context of fileContexts) {
            const shortFileName = FileUtils.getFileName(context.file);
            const filePath = context.file;
            const normalizedPath = FileUtils.normalizePath(filePath);
            // Try to find changed line ranges using different path formats
            let changedRanges;
            // Try exact path match first
            changedRanges = changedLinesRanges.get(normalizedPath);
            // If not found, try relative path variants
            if (!changedRanges) {
                const fileName = FileUtils.getFileName(normalizedPath);
                for (const [changedPath] of changedLinesRanges) {
                    if (changedPath.endsWith(fileName) ||
                        changedPath.includes(fileName)) {
                        changedRanges = changedLinesRanges.get(changedPath);
                        break;
                    }
                }
            }
            // Separate hints for this file into changed and remaining
            const changedHints = [];
            const remainingHints = [];
            for (const hint of context.hints) {
                // Find the file context for this hint using className
                const hintFileName = hint.className || '';
                const fileContext = fileContextMap.get(hintFileName);
                if (!fileContext || !changedRanges || changedRanges.length === 0) {
                    // If no file context or no changed ranges, it's a remaining hint
                    remainingHints.push(hint);
                    continue;
                }
                // Check if this hint's line falls within any changed range
                const hintStartLine = hint.region.startLine ?? 1;
                const hintEndLine = hint.region.endLine ?? hintStartLine;
                const isInChangedLines = changedRanges.some((range) => (hintStartLine >= range.start && hintStartLine <= range.end) ||
                    (hintEndLine >= range.start && hintEndLine <= range.end) ||
                    (hintStartLine <= range.start && hintEndLine >= range.end));
                if (isInChangedLines) {
                    changedHints.push(hint);
                }
                else {
                    remainingHints.push(hint);
                }
            }
            // Store the separated hints for this file
            if (changedHints.length > 0) {
                changedLinesHintsPerFile.set(shortFileName, changedHints);
            }
            if (remainingHints.length > 0) {
                remainingHintsPerFile.set(shortFileName, remainingHints);
            }
        }
        return { changedLinesHintsPerFile, remainingHintsPerFile };
    }
    /**
     * Separate AI analysis entries into changed lines and remaining entries
     *
     * @param allTableEntries All AI analysis table entries
     * @param changedLinesHints Hints that are on changed lines
     * @returns Object containing changedLineEntries and remainingEntries
     */
    separateAIAnalysisByChangedLines(allTableEntries, changedLinesHints) {
        const changedLinesAIAnalysis = [];
        const remainingIssuesAIAnalysis = [];
        // Create a set of changed line hint identifiers for quick lookup
        const changedLineIdentifiers = new Set(changedLinesHints.map((hint) => `${hint.className}-${hint.region.startLine ?? 1}`));
        for (const entry of allTableEntries) {
            const entryIdentifier = `${entry.className}-${entry.lineNumber}`;
            if (changedLineIdentifiers.has(entryIdentifier)) {
                changedLinesAIAnalysis.push(entry);
            }
            else {
                remainingIssuesAIAnalysis.push(entry);
            }
        }
        return { changedLinesAIAnalysis, remainingIssuesAIAnalysis };
    }
    /**
     * Build comprehensive code snippet for AI analysis by concatenating all file contents
     *
     * @param fileContexts Array of file contexts containing file names and content
     * @returns Formatted code snippet string with file separators for AI consumption
     * @example
     * ```typescript
     * const codeSnippet = this.buildCodeSnippet(fileContexts);
     * ```
     */
    buildCodeSnippet(fileContexts) {
        let comprehensiveCodeSnippet = '';
        for (const { file, content } of fileContexts) {
            const shortFileName = FileUtils.getFileName(file);
            comprehensiveCodeSnippet += `\n=== FILE: ${shortFileName} ===\n`;
            comprehensiveCodeSnippet += content;
            comprehensiveCodeSnippet += '\n\n';
        }
        return comprehensiveCodeSnippet;
    }
    /**
     * Post PR comment with the review results, either to git provider or local output
     *
     * @param message The comprehensive review message to post
     * @param flags Command line flags containing PR and git configuration
     * @param commitSha Git commit SHA for associating the comment with specific commit
     * @param gitProvider Optional git provider for posting comments to remote repository
     * @returns Promise that resolves when comment posting is complete
     * @throws Error if PR comment posting fails when git provider is configured
     */
    async postPRComment(message, flags, commitSha, gitProvider) {
        const prCommentRequest = {
            prNumber: flags['pull-request-id'] ?? '',
            message,
            suggestedCodeChange: undefined,
            sourceFile: '',
            startLine: 0,
            endLine: 0,
            commitId: commitSha,
        };
        if (flags['pull-request-id'] && gitProvider) {
            try {
                await gitProvider.upsertPRComment(prCommentRequest);
                Metrics.gauge('ai_review.pr_comment_posted_success', 1);
            }
            catch (error) {
                Logger.error('Failed to add comprehensive PR comment:', error);
                Metrics.gauge('ai_review.pr_comment_posted_failure', 1);
            }
        }
        else {
            Logger.debug(`No PR ID or git provider configured, outputting review locally. Generated review: ${message}`);
        }
    }
    /**
     * Build comprehensive PMD review comment including AI analysis and Docusign-specific issues
     * Shows both total vulnerabilities and vulnerabilities only on changed lines.
     *
     * @param aiResponse Response from AI provider containing code review comments
     * @param fileContexts Array of file contexts with content and associated hints
     * @returns Formatted markdown message for PR comment with issues breakdown and AI analysis table
     * @example
     * ```typescript
     * const message = this.buildReviewComment(aiResponse, fileContexts);
     * ```
     */
    buildReviewComment(aiResponse, fileContexts, changedLinesRanges) {
        // Separate hints into changed lines and remaining hints in one pass
        const { changedLinesHintsPerFile, remainingHintsPerFile } = this.separateHintsByChangedLines(fileContexts, changedLinesRanges);
        // Get Docusign specific issues per file
        const docuSignIssuesPerFile = this.getDocuSignIssues(fileContexts);
        // Calculate counts from pre-computed maps
        let changedLinesIssueCount = 0;
        for (const fileHints of changedLinesHintsPerFile.values()) {
            changedLinesIssueCount += fileHints.length;
        }
        let remainingIssueCount = 0;
        for (const fileHints of remainingHintsPerFile.values()) {
            remainingIssueCount += fileHints.length;
        }
        // Calculate total DocuSign issues count
        let docuSignIssuesCount = 0;
        for (const fileIssues of docuSignIssuesPerFile.values()) {
            docuSignIssuesCount += fileIssues.length;
        }
        // Calculate total hints count from all file contexts
        const totalHintsCount = changedLinesIssueCount + remainingIssueCount;
        const totalIssues = totalHintsCount + docuSignIssuesCount;
        // Build the review message header
        let changedLinesIssuesMessage = '';
        let docusignIssuesMessage = '';
        let remainingIssuesMessage = '';
        let message = '';
        // Build the summary message with total issues
        let summaryMessage = '### 📊 Vulnerability Summary ⬇️ \n' +
            `- **Total Vulnerabilities:** ${totalIssues} across ${fileContexts.length} file(s)\n`;
        if (changedLinesIssueCount > 0) {
            summaryMessage += `- **Vulnerabilities in New/Modified Code:** ${changedLinesIssueCount} (Most critical; review first)\n`;
            changedLinesIssuesMessage += `<details><summary>‼️ ${changedLinesIssueCount} Vulnerabilities in New/Modified Code (Fix these first – they're in the code you just changed!)</summary>\n\n`;
        }
        if (docuSignIssuesCount > 0) {
            summaryMessage += `- **Docusign-Specific Vulnerabilities:** ${docuSignIssuesCount} (High priority due to specific requirements)\n`;
            docusignIssuesMessage += `<details><summary>🏢 ${docuSignIssuesCount} Docusign-Specific Vulnerabilities (These are crucial for DocuSign compliance – address them next!)</summary>\n\n`;
        }
        if (remainingIssueCount > 0) {
            summaryMessage += `- **Vulnerabilities in Existing unchanged Code:** ${remainingIssueCount} (Address as time permits)\n`;
            remainingIssuesMessage += `<details><summary>📄 ${remainingIssueCount} Vulnerabilities in Existing Code (Found in unchanged code; fix these when you have time)</summary>\n\n`;
        }
        message += summaryMessage += '\n---\n\n';
        // Build file analysis content for Changed lines, Docusign issues and remaining issues
        let codeAnalysisMessage = '### 🔎 Detailed Vulnerability Breakdown ⬇️\n\n';
        const processedFiles = new Set();
        const metricsPerType = {
            issueViolationsPerType: {},
            issueFilesPerType: {},
            docusignIssuesPerType: {},
            docusignFilesPerType: {},
        };
        for (const { file, hints } of fileContexts) {
            const shortFileName = FileUtils.getFileName(file);
            const fileType = FileUtils.getFileExtension(file);
            // Skip if we've already processed this file
            if (processedFiles.has(shortFileName)) {
                Logger.debug(`Skipping duplicate file: ${shortFileName}`);
                continue;
            }
            processedFiles.add(shortFileName);
            // Get Changed, DocuSign specific and Remaining issues for this file
            const changedHints = changedLinesHintsPerFile.get(shortFileName) ?? [];
            const remainingHints = remainingHintsPerFile.get(shortFileName) ?? [];
            const fileDocuSignIssues = docuSignIssuesPerFile.get(shortFileName) ?? [];
            // Count violations and #files per type from file type
            metricsPerType.issueFilesPerType[fileType] =
                (metricsPerType.issueFilesPerType[fileType] ?? 0) + 1;
            metricsPerType.issueViolationsPerType[fileType] =
                (metricsPerType.issueViolationsPerType[fileType] ?? 0) + hints.length;
            // Count Docusign violations and #files per type from file type
            if (fileDocuSignIssues.length > 0) {
                metricsPerType.docusignFilesPerType[fileType] =
                    (metricsPerType.docusignFilesPerType[fileType] ?? 0) + 1;
                metricsPerType.docusignIssuesPerType[fileType] =
                    (metricsPerType.docusignIssuesPerType[fileType] ?? 0) +
                        fileDocuSignIssues.length;
            }
            // Add changed line issues for the file
            if (changedHints.length > 0) {
                changedLinesIssuesMessage += `#### 🆕 ${shortFileName} (${changedHints.length} issues)\n`;
                for (const hint of changedHints) {
                    changedLinesIssuesMessage += `- **Line ${hint.region.startLine ?? 1}**: [${hint.ruleId}] ${hint.message}\n`;
                }
                changedLinesIssuesMessage += '\n';
            }
            // Add DocuSign specific issues for this file
            if (fileDocuSignIssues.length > 0) {
                docusignIssuesMessage += `#### 🏢 ${shortFileName} (${fileDocuSignIssues.length} issues)\n`;
                for (const issue of fileDocuSignIssues) {
                    docusignIssuesMessage += `- **Line ${issue.lineNumber}**: [${issue.ruleType}] ${issue.issue}\n`;
                }
                docusignIssuesMessage += '\n';
            }
            // Add remaining issues (not in changed lines)
            if (remainingHints.length > 0) {
                remainingIssuesMessage += `#### 📄 ${shortFileName} (${remainingHints.length} issues)\n`;
                for (const hint of remainingHints) {
                    remainingIssuesMessage += `- **Line ${hint.region.startLine ?? 1}**: [${hint.ruleId}] ${hint.message}\n`;
                }
            }
            Logger.debug(`Processed File ${shortFileName} has ${changedHints.length} changed line hints, ${remainingHints.length} remaining hints, and ${fileDocuSignIssues.length} Docusign issues`, {
                changedHints,
                remainingHints,
                docuSignIssues: fileDocuSignIssues,
            });
        }
        // Section 1: Changed Lines Analysis (PRIORITY - shown first)
        if (changedLinesIssuesMessage.trim()) {
            codeAnalysisMessage += changedLinesIssuesMessage + '</details>';
        }
        // Section 2: Docusign Specific Issues
        if (docusignIssuesMessage.trim()) {
            codeAnalysisMessage += docusignIssuesMessage + '</details>';
        }
        // Section 3: Remaining Issues Analysis
        if (remainingIssuesMessage.trim()) {
            codeAnalysisMessage += remainingIssuesMessage + '</details>';
        }
        codeAnalysisMessage += '\n\n---\n\n';
        message += codeAnalysisMessage;
        // Log the metrics for observability
        Logger.debug(`Total issues calculated: ${totalIssues} = SCA (${totalHintsCount}) + DocuSign (${docuSignIssuesCount}). ` +
            `Changed line issues: ${changedLinesIssueCount}`, metricsPerType);
        Metrics.gauge('ai_review.total_issues', totalIssues);
        Metrics.gauge('ai_review.changed_lines_issues', changedLinesIssueCount);
        Metrics.gauge('ai_review.docusign_specific_issues', docuSignIssuesCount);
        Metrics.gauge('ai_review.remaining_issues', remainingIssueCount);
        for (const [metricCategory, metricData] of Object.entries(metricsPerType)) {
            for (const [type, count] of Object.entries(metricData)) {
                if (!count || count === 0)
                    continue; // Skip zero counts
                // Convert camelCase to snake_case for metric names
                const metricName = metricCategory
                    .replace(/([A-Z])/g, '_$1')
                    .toLowerCase();
                Metrics.gauge(`ai_review.${metricName}.${type}`, count);
            }
        }
        // AI Suggestion Table - Include AI comments AND DocuSign specific issues
        const hasAiComments = aiResponse.comments && aiResponse.comments.length > 0;
        if (hasAiComments || docuSignIssuesCount > 0) {
            // Add Docusign specific entries to AI Suggestion table
            const docusignIssuesAIAnalysis = this.addDocuSignAnalysis(fileContexts);
            let allTableEntries = [];
            // Add AI-generated entries
            if (hasAiComments) {
                allTableEntries = this.addAIGeneratedAnalysis(aiResponse.comments, fileContexts);
            }
            // Convert changed lines hints map to array for the method
            const changedLinesHintsArray = [];
            for (const fileHints of changedLinesHintsPerFile.values()) {
                changedLinesHintsArray.push(...fileHints);
            }
            // Split AI analysis into changed lines and remaining
            const { changedLinesAIAnalysis, remainingIssuesAIAnalysis } = this.separateAIAnalysisByChangedLines(allTableEntries, changedLinesHintsArray);
            // Build the AI analysis message with sections
            let aiAnalysisMessage = '### 💡 AI Recommendations ⬇️\n\n';
            // Section 1: AI Suggestion for Changed Lines Issues
            aiAnalysisMessage += this.generateAIAnalysisSection('‼️ Vulnerabilities in New/Modified Code (Check this first)', changedLinesAIAnalysis);
            // Section 2: AI Suggestion for Docusign Specific Issues
            aiAnalysisMessage += this.generateAIAnalysisSection('🏢 Docusign-Specific Vulnerabilities (Address these next)', docusignIssuesAIAnalysis);
            // Section 3: AI Suggestion for Other Issues
            aiAnalysisMessage += this.generateAIAnalysisSection('📄 Vulnerabilities in Existing Code (Fix when time permits)', remainingIssuesAIAnalysis);
            message += aiAnalysisMessage;
        }
        Logger.debug(`Final AI-review message: \n ${message}`, message);
        return message;
    }
    /**
     * Generate a formatted AI analysis table section with header, description, and entries
     *
     * @param sectionTitle The title for the AI analysis section (e.g., "Changed Files", "Docusign Specific Issues")
     * @param entries Array of table entries to display
     * @param description Optional custom description for the section
     * @returns Formatted markdown string for the AI analysis section
     * @example
     * ```typescript
     * const section = this.generateAIAnalysisSection("Changed Files", changedLineEntries, "issues in your modifications");
     * ```
     */
    generateAIAnalysisSection(sectionTitle, entries) {
        if (entries.length === 0) {
            return '';
        }
        let section = `<details> <summary>${sectionTitle} (${entries.length})</summary>\n\n\n`;
        section += REVIEW_COMMAND_HINTS_DEFAULTS.AI_ANALYSIS_TABLE_HEADERS;
        // Sort by class name, then by line number
        const sortedEntries = [...entries].sort((a, b) => {
            if (a.className !== b.className) {
                return a.className.localeCompare(b.className);
            }
            return a.lineNumber - b.lineNumber;
        });
        // Generate table rows
        for (const entry of sortedEntries) {
            const lineInfo = entry.lineNumber > 0 ? entry.lineNumber.toString() : 'N/A';
            section += `| ${entry.className} | ${lineInfo} | ${entry.ruleType} | ${entry.issue} | ${entry.suggestedFix} |\n`;
        }
        section += '</details>\n\n';
        return section;
    }
    /**
     * Add Docusign-specific entries to the analysis table, including custom pattern checks
     * for JavaScript/TypeScript files and ESLint violations from static analysis hints
     *
     * @param fileContexts Array of file contexts containing file information, content, and hints
     * @param tableEntries Array to populate with Docusign-specific table entries
     * @returns tableEntries - Array of TableEntry objects with Docusign-specific issues
     * @example
     * ```typescript
     * this.addDocuSignAnalysis(fileContexts, tableEntries);
     * ```
     */
    addDocuSignAnalysis(fileContexts) {
        const tableEntries = [];
        for (const { file, content } of fileContexts) {
            // Process Apex files
            if (FileUtils.isApexFile(file)) {
                const shortFileName = FileUtils.getFileName(file);
                // Test framework check
                if (FileUtils.isTestFile(file, content) &&
                    DocuSignRuleUtils.hasOldTestFramework(content)) {
                    tableEntries.push({
                        className: `${shortFileName}`,
                        lineNumber: 1,
                        ruleType: DOCUSIGN_SPECIFIC_ISSUES.TEST_FRAMEWORK.ruleType,
                        issue: DOCUSIGN_SPECIFIC_ISSUES.TEST_FRAMEWORK.issue,
                        suggestedFix: DOCUSIGN_SPECIFIC_ISSUES.TEST_FRAMEWORK.suggestedFix,
                    });
                }
                // Schema.getGlobalDescribe check
                const globalDescribeUsages = DocuSignRuleUtils.hasGlobalDescribeUsage(content);
                if (globalDescribeUsages.length > 0) {
                    for (const issue of globalDescribeUsages) {
                        const lineNumber = issue.lineNumber ?? 1;
                        tableEntries.push({
                            className: `${shortFileName}`,
                            lineNumber,
                            ruleType: DOCUSIGN_SPECIFIC_ISSUES.GLOBAL_DESCRIBE_USAGE.ruleType,
                            issue: DOCUSIGN_SPECIFIC_ISSUES.GLOBAL_DESCRIBE_USAGE.issue.replace('$line_number', lineNumber.toString()),
                            suggestedFix: DOCUSIGN_SPECIFIC_ISSUES.GLOBAL_DESCRIBE_USAGE.suggestedFix,
                        });
                    }
                }
            }
        }
        return tableEntries;
    }
    /**
     * Add AI-generated entries to the analysis table, filtering by PMD class name matches
     *
     * @param comments Array of AI-generated review comments
     * @param fileContexts Array of file contexts for class name matching
     * @param tableEntries Array to populate with AI-generated table entries
     * @returns void - method modifies tableEntries array in place
     * @example
     * ```typescript
     * this.addAIGeneratedAnalysis(aiResponse.comments, fileContexts, tableEntries);
     * ```
     */
    addAIGeneratedAnalysis(comments, fileContexts) {
        const tableEntries = [];
        if (!comments) {
            return tableEntries;
        }
        // Build set of PMD classNames (with extension) for exact matching
        const pmdClassNames = new Set();
        for (const { file, hints } of fileContexts) {
            const shortFileName = file.split('/').pop() ?? file;
            // Add the full filename with extension
            pmdClassNames.add(shortFileName);
            // Also add className from hints if available (with extension)
            for (const hint of hints) {
                if (hint.className) {
                    pmdClassNames.add(hint.className);
                }
            }
        }
        for (const comment of comments) {
            // Simple matching: only add AI comment if its className (with extension) matches a PMD className
            if (comment.className && pmdClassNames.has(comment.className)) {
                const messageText = comment.message ?? '';
                const formattedMessage = messageText.replace(/\n/g, ' ');
                const suggestionText = comment.suggestion ?? '';
                const formattedSuggestion = suggestionText
                    ? suggestionText.replace(/\n/g, ' ')
                    : REVIEW_COMMAND_HINTS_DEFAULTS.SUGGESTION;
                tableEntries.push({
                    className: comment.className,
                    lineNumber: comment.startLine ?? 1,
                    ruleType: REVIEW_COMMAND_HINTS_DEFAULTS.SUGGESTION_RULE_TYPE,
                    issue: formattedMessage,
                    suggestedFix: formattedSuggestion,
                });
            }
        }
        return tableEntries;
    }
    /**
     * Get Docusign-specific issues for display in Code Quality Review section, grouped by file
     *
     * @param fileContexts Array of file contexts to analyze for Docusign-specific patterns
     * @returns Map of file names to arrays of DocuSign-specific issues
     * @example
     * ```typescript
     * const issuesPerFile = this.getDocuSignIssues(fileContexts);
     * console.log(`Found DocuSign issues in ${issuesPerFile.size} files`);
     * ```
     */
    getDocuSignIssues(fileContexts) {
        const issuesPerFile = new Map();
        for (const { file, content } of fileContexts) {
            const shortFileName = file.split('/').pop() ?? file;
            const fileIssues = [];
            // Test framework check
            if (shortFileName.toLowerCase().includes(TEST_PATTERNS.TEST_FILE_PATTERN) ||
                content.includes(TEST_PATTERNS.APEX_TEST_ANNOTATION)) {
                const hasNewTestFramework = content.includes(TEST_PATTERNS.SOBJECT_MOCKER) ||
                    content.includes(TEST_PATTERNS.CLASS_MOCKER) ||
                    content.includes(TEST_PATTERNS.GENERATE_MOCK_SOBJECT);
                if (!hasNewTestFramework) {
                    fileIssues.push({
                        lineNumber: 1,
                        ruleType: DOCUSIGN_SPECIFIC_ISSUES.TEST_FRAMEWORK.ruleType,
                        issue: DOCUSIGN_SPECIFIC_ISSUES.TEST_FRAMEWORK.issue,
                    });
                }
            }
            // Schema.getGlobalDescribe check
            if (content.includes('Schema.getGlobalDescribe()')) {
                const lines = content.split('\n');
                for (let i = 0; i < lines.length; i++) {
                    if (lines[i].includes('Schema.getGlobalDescribe()')) {
                        fileIssues.push({
                            lineNumber: i + 1,
                            ruleType: DOCUSIGN_SPECIFIC_ISSUES.GLOBAL_DESCRIBE_USAGE.ruleType,
                            issue: DOCUSIGN_SPECIFIC_ISSUES.GLOBAL_DESCRIBE_USAGE.issue.replace('$line_number', (i + 1).toString()),
                        });
                        break; // Only show first occurrence
                    }
                }
            }
            // Add to map only if there are issues for this file
            if (fileIssues.length > 0) {
                issuesPerFile.set(shortFileName, fileIssues);
            }
        }
        return issuesPerFile;
    }
    /**
     * Run hybrid analysis workflow combining SCA for Salesforce files and PMD for other file types
     *
     * @param repoDir Path to the repository directory containing files to analyze
     * @param flags Command line flags containing configuration file paths
     * @returns Promise resolving to a map of file paths to review hints from combined analysis
     * @throws Error if analysis workflow fails critically
     * @example
     * ```typescript
     * const fileMap = await this.runHybridAnalysis(repoDir, flags);
     * ```
     */
    async runHybridAnalysis(repoDir, flags) {
        const combinedFileMap = new Map();
        try {
            // Find all supported files and categorize them
            const allFiles = this.findAllSupportedFiles(repoDir);
            if (allFiles.length === 0) {
                Logger.debug('No supported files found for analysis, so skipping the process');
                return combinedFileMap;
            }
            // Categorize files into Salesforce and non-Salesforce groups
            const { salesforceFiles, nonSalesforceFiles } = this.categorizeFiles(allFiles);
            Metrics.gauge('ai_review.code_analyzer_file_count', salesforceFiles.length);
            Metrics.gauge('ai_review.pmd_analysis_file_count', nonSalesforceFiles.length);
            // Run SCA on Salesforce files
            if (salesforceFiles.length > 0) {
                // Get SCA analysis
                const scaResults = await this.runScaAnalysis(repoDir, salesforceFiles, flags['sf-config-file']);
                this.mergeFileMaps(combinedFileMap, scaResults);
                // Get SCA violations count for metrics
                let scaViolationsCount = 0;
                for (const hints of scaResults.values()) {
                    scaViolationsCount += hints.length;
                }
                if (scaViolationsCount > 0) {
                    Metrics.gauge('ai_review.sca_violations_count', scaViolationsCount);
                }
                Logger.debug(`SCA found issues in ${scaResults.size} files with ${scaViolationsCount} violations`);
            }
            // Run PMD on non-Salesforce files
            if (nonSalesforceFiles.length > 0) {
                const pmdResults = await this.runPmdAnalysis(repoDir, nonSalesforceFiles, flags['pmd-config-file']);
                this.mergeFileMaps(combinedFileMap, pmdResults);
                // Get SCA violations count for metrics
                let pmdViolationsCount = 0;
                for (const hints of pmdResults.values()) {
                    pmdViolationsCount += hints.length;
                }
                if (pmdViolationsCount > 0) {
                    Metrics.gauge('ai_review.pmd_violations_count', pmdViolationsCount);
                }
                Logger.debug(`PMD found issues in ${pmdResults.size} files with ${pmdViolationsCount} violations`);
            }
            return combinedFileMap;
        }
        catch (error) {
            Logger.error('Error running hybrid analysis:', error);
            return combinedFileMap;
        }
    }
    /**
     * Categorize files into Salesforce and non-Salesforce groups based on file extensions
     *
     * @param files Array of file paths to categorize
     * @returns Object containing arrays of salesforceFiles and nonSalesforceFiles
     * @example
     * ```typescript
     * const { salesforceFiles, nonSalesforceFiles } = this.categorizeFiles(allFiles);
     * ```
     */
    categorizeFiles(files) {
        const salesforceExtensions = SUPPORTED_SALESFORCE_EXTENSIONS;
        const salesforceFiles = [];
        const nonSalesforceFiles = [];
        for (const file of files) {
            const extension = path.extname(file).toLowerCase();
            if (salesforceExtensions.includes(extension)) {
                salesforceFiles.push(file);
            }
            else {
                nonSalesforceFiles.push(file);
            }
        }
        return { salesforceFiles, nonSalesforceFiles };
    }
    /**
     * Run Salesforce Code Analyzer (SCA) on Salesforce-specific files
     *
     * @param repoDir Path to the repository directory
     * @param salesforceFiles Array of Salesforce file paths to analyze
     * @param sfConfigFile Optional path to SCA configuration file
     * @returns Promise resolving to a map of file paths to review hints from SCA analysis
     * @throws Error if SCA analysis fails or temp directory operations fail
     */
    async runScaAnalysis(repoDir, salesforceFiles, sfConfigFile) {
        const fileMap = new Map();
        try {
            // Create a temporary directory with only Salesforce files for SCA
            const tempDir = path.join(repoDir, 'sca-temp');
            await fse.ensureDir(tempDir);
            // Copy Salesforce files to temp directory maintaining structure
            const copyPromises = salesforceFiles.map(async (file) => {
                const relativePath = path.relative(repoDir, file);
                const destPath = path.join(tempDir, relativePath);
                await fse.ensureDir(path.dirname(destPath));
                await fse.copy(file, destPath);
            });
            await Promise.all(copyPromises);
            // Use default SCA config file if none provided
            const scaConfigFile = sfConfigFile ?? this.getDefaultScaConfigFile();
            // Run SCA on the temp directory
            const scatProvider = SCATProviderFactory.getInstance(SCATProviderType.SFCodeAnalyzer, tempDir, scaConfigFile);
            Logger.debug(`SCA using config file: \n ${scaConfigFile}`);
            const sarifData = await scatProvider.run();
            // Convert SARIF to ReviewHint format
            const scaFileMap = AIPRReview.createFileMap(sarifData);
            // Map temp paths back to original paths
            for (const [tempFilePath, hints] of scaFileMap) {
                const originalPath = tempFilePath.replace(tempDir, repoDir);
                fileMap.set(originalPath, hints);
            }
            // Clean up temp directory
            fs.rmdirSync(tempDir, { recursive: true });
        }
        catch (error) {
            Logger.error('Error running SCA analysis:', error);
            // Clean up temp directory on error
            const tempDir = path.join(repoDir, 'sca-temp');
            try {
                fs.rmdirSync(tempDir, { recursive: true });
            }
            catch (cleanupError) {
                Logger.error('Error cleaning up temp directory:', cleanupError);
            }
        }
        return fileMap;
    }
    /**
     * Run PMD static analysis on non-Salesforce files using SCATProvider
     *
     * @param repoDir Path to the repository directory
     * @param nonSalesforceFiles Array of non-Salesforce file paths to analyze
     * @param pmdConfigFile Optional path to PMD configuration file
     * @returns Promise resolving to a map of file paths to review hints from PMD analysis
     * @throws Error if PMD analysis fails beyond expected violation exit codes
     */
    async runPmdAnalysis(repoDir, nonSalesforceFiles, pmdConfigFile) {
        const fileMap = new Map();
        try {
            if (nonSalesforceFiles.length === 0) {
                Logger.debug('No non-Salesforce files to analyze with PMD');
                return fileMap;
            }
            // Use default PMD config file if none provided
            const pmdConfig = pmdConfigFile ?? this.getDefaultPmdConfigFile();
            // Create PMD provider instance using SCAT factory
            const pmdProvider = SCATProviderFactory.getInstance(SCATProviderType.PMDCodeAnalyzer, repoDir, pmdConfig, nonSalesforceFiles);
            Logger.debug(`PMD using config file: \n ${pmdConfig}`);
            const sarifData = await pmdProvider.run();
            // Convert SARIF to ReviewHint format
            const pmdFileMap = AIPRReview.createFileMap(sarifData);
            // Copy the results to our return map
            for (const [filePath, hints] of pmdFileMap) {
                fileMap.set(filePath, hints);
            }
        }
        catch (error) {
            Logger.error('Error running PMD analysis on non-Salesforce files:', error);
        }
        return fileMap;
    }
    /**
     * Merge two file maps by combining hints for existing files and adding new file entries
     *
     * @param target Target map to merge results into (modified in place)
     * @param source Source map containing additional results to merge
     * @returns void - modifies target map in place
     * @example
     * ```typescript
     * this.mergeFileMaps(combinedFileMap, scaResults);
     * ```
     */
    mergeFileMaps(target, source) {
        for (const [filePath, hints] of source) {
            if (target.has(filePath)) {
                // Merge hints if file already exists
                target.get(filePath).push(...hints);
            }
            else {
                // Add new file entry
                target.set(filePath, [...hints]);
            }
        }
    }
    /**
     * Find all supported files for analysis by scanning directory recursively
     *
     * @param repoDir Path to repository directory to scan
     * @returns Array of file paths that match supported extensions for analysis
     * @example
     * ```typescript
     * const supportedFiles = this.findAllSupportedFiles('/path/to/repo');
     * console.log(`Found ${supportedFiles.length} files to analyze`);
     * ```
     */
    findAllSupportedFiles(repoDir) {
        const supportedFiles = [];
        // Create an array from all supported extensions dynamically
        const allExtensions = Object.values(SUPPORTED_FILE_EXTENSIONS).flatMap((extensions) => extensions.map((ext) => ext.toLowerCase()));
        try {
            const findFiles = (dir) => {
                const items = fs.readdirSync(dir, { withFileTypes: true });
                for (const item of items) {
                    const fullPath = path.join(dir, item.name);
                    if (item.isDirectory()) {
                        // Skip common non-source directories
                        if (![...EXCLUDED_DIRECTORIES].includes(item.name)) {
                            findFiles(fullPath);
                        }
                    }
                    else if (allExtensions.some((ext) => item.name.toLowerCase().endsWith(ext))) {
                        supportedFiles.push(fullPath);
                    }
                }
            };
            findFiles(repoDir);
        }
        catch (error) {
            Logger.error('Error scanning for supported files:', error);
        }
        return supportedFiles;
    }
    /**
     * Get the default SCA configuration file path from the built library directory
     *
     * @returns Path to the default SCA configuration file
     * @throws Error if the default configuration file is not found
     * @example
     * ```typescript
     * const configPath = this.getDefaultScaConfigFile();
     * ```
     */
    getDefaultScaConfigFile() {
        // Use the built version path (lib directory)
        const builtConfigPath = path.join(currentDirname, COMMAND_DEFAULTS.SCA_RULESET);
        if (fs.existsSync(builtConfigPath)) {
            return builtConfigPath;
        }
        // If built config file doesn't exist, throw an error
        throw new Error(`Default SCA configuration file not found at: ${builtConfigPath}. ` +
            'Please ensure the build process copies configuration files correctly.');
    }
    /**
     * Get the default PMD configuration file path from the built library directory
     *
     * @returns Path to the default PMD configuration file
     * @throws Error if the default configuration file is not found
     * @example
     * ```typescript
     * const configPath = this.getDefaultPmdConfigFile();
     * ```
     */
    getDefaultPmdConfigFile() {
        // Use the built version path (lib directory)
        const builtConfigPath = path.join(currentDirname, COMMAND_DEFAULTS.PMD_RULESET);
        if (fs.existsSync(builtConfigPath)) {
            return builtConfigPath;
        }
        // If built config file doesn't exist, throw an error
        throw new Error(`Default PMD configuration file not found at: ${builtConfigPath}. ` +
            'Please ensure the build process copies configuration files correctly.');
    }
}