|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const { getEnricher, getClient, isCodeIntelAvailable } = require('../index'); |
| 4 | + |
| 5 | +// Risk level thresholds based on blast radius (reference count) |
| 6 | +const RISK_THRESHOLDS = { |
| 7 | + LOW_MAX: 4, // 0-4 refs = LOW |
| 8 | + MEDIUM_MAX: 15, // 5-15 refs = MEDIUM |
| 9 | + // >15 refs = HIGH |
| 10 | +}; |
| 11 | + |
| 12 | +// Minimum references to suggest REUSE (>threshold = REUSE, <=threshold = ADAPT) |
| 13 | +const REUSE_MIN_REFS = 2; |
| 14 | + |
| 15 | +/** |
| 16 | + * DevHelper — Code intelligence helper for @dev agent tasks. |
| 17 | + * |
| 18 | + * All functions return null gracefully when no provider is available. |
| 19 | + * Never throws — safe to call unconditionally in task workflows. |
| 20 | + */ |
| 21 | + |
| 22 | +/** |
| 23 | + * Check for duplicates and similar code before writing a new file or function. |
| 24 | + * Used by IDS Gate G4 in dev-develop-story and build-autonomous tasks. |
| 25 | + * |
| 26 | + * @param {string} fileName - Name of the file/function to be created |
| 27 | + * @param {string} description - Description of what it does |
| 28 | + * @returns {Promise<{duplicates: Object, references: Array, suggestion: string}|null>} |
| 29 | + */ |
| 30 | +async function checkBeforeWriting(fileName, description) { |
| 31 | + if (!isCodeIntelAvailable()) { |
| 32 | + return null; |
| 33 | + } |
| 34 | + |
| 35 | + try { |
| 36 | + const enricher = getEnricher(); |
| 37 | + const dupes = await enricher.detectDuplicates(description, { path: '.' }); |
| 38 | + |
| 39 | + // Also search for the fileName as a symbol reference |
| 40 | + const client = getClient(); |
| 41 | + const nameRefs = await client.findReferences(fileName); |
| 42 | + |
| 43 | + const hasMatches = (dupes && dupes.matches && dupes.matches.length > 0) || |
| 44 | + (nameRefs && nameRefs.length > 0); |
| 45 | + |
| 46 | + if (!hasMatches) { |
| 47 | + return null; |
| 48 | + } |
| 49 | + |
| 50 | + return { |
| 51 | + duplicates: dupes, |
| 52 | + references: nameRefs || [], |
| 53 | + suggestion: _formatSuggestion(dupes, nameRefs), |
| 54 | + }; |
| 55 | + } catch { |
| 56 | + return null; |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Suggest reuse of an existing symbol instead of creating a new one. |
| 62 | + * Searches for definitions and references to determine REUSE vs ADAPT. |
| 63 | + * |
| 64 | + * @param {string} symbol - Symbol name to search for |
| 65 | + * @returns {Promise<{file: string, line: number, references: number, suggestion: string}|null>} |
| 66 | + */ |
| 67 | +async function suggestReuse(symbol) { |
| 68 | + if (!isCodeIntelAvailable()) { |
| 69 | + return null; |
| 70 | + } |
| 71 | + |
| 72 | + try { |
| 73 | + const client = getClient(); |
| 74 | + const [definition, refs] = await Promise.all([ |
| 75 | + client.findDefinition(symbol), |
| 76 | + client.findReferences(symbol), |
| 77 | + ]); |
| 78 | + |
| 79 | + if (!definition && (!refs || refs.length === 0)) { |
| 80 | + return null; |
| 81 | + } |
| 82 | + |
| 83 | + const refCount = refs ? refs.length : 0; |
| 84 | + // REUSE if widely used, ADAPT if exists but lightly used |
| 85 | + const suggestion = refCount > REUSE_MIN_REFS ? 'REUSE' : 'ADAPT'; |
| 86 | + |
| 87 | + return { |
| 88 | + file: definition ? definition.file : (refs[0] ? refs[0].file : null), |
| 89 | + line: definition ? definition.line : (refs[0] ? refs[0].line : null), |
| 90 | + references: refCount, |
| 91 | + suggestion, |
| 92 | + }; |
| 93 | + } catch { |
| 94 | + return null; |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +/** |
| 99 | + * Get naming conventions and patterns for a given path. |
| 100 | + * Used to ensure new code follows existing project conventions. |
| 101 | + * |
| 102 | + * @param {string} targetPath - Path to analyze conventions for |
| 103 | + * @returns {Promise<{patterns: Array, stats: Object}|null>} |
| 104 | + */ |
| 105 | +async function getConventionsForPath(targetPath) { |
| 106 | + if (!isCodeIntelAvailable()) { |
| 107 | + return null; |
| 108 | + } |
| 109 | + |
| 110 | + try { |
| 111 | + const enricher = getEnricher(); |
| 112 | + return await enricher.getConventions(targetPath); |
| 113 | + } catch { |
| 114 | + return null; |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +/** |
| 119 | + * Assess refactoring impact with blast radius and risk level. |
| 120 | + * Used by dev-suggest-refactoring to show impact before changes. |
| 121 | + * |
| 122 | + * @param {string[]} files - Files to assess impact for |
| 123 | + * @returns {Promise<{blastRadius: number, riskLevel: string, references: Array, complexity: Object}|null>} |
| 124 | + */ |
| 125 | +async function assessRefactoringImpact(files) { |
| 126 | + if (!isCodeIntelAvailable()) { |
| 127 | + return null; |
| 128 | + } |
| 129 | + |
| 130 | + try { |
| 131 | + const enricher = getEnricher(); |
| 132 | + const impact = await enricher.assessImpact(files); |
| 133 | + |
| 134 | + if (!impact) { |
| 135 | + return null; |
| 136 | + } |
| 137 | + |
| 138 | + return { |
| 139 | + blastRadius: impact.blastRadius, |
| 140 | + riskLevel: _calculateRiskLevel(impact.blastRadius), |
| 141 | + references: impact.references, |
| 142 | + complexity: impact.complexity, |
| 143 | + }; |
| 144 | + } catch { |
| 145 | + return null; |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +/** |
| 150 | + * Format a Code Intelligence Suggestion message from duplicate detection results. |
| 151 | + * @param {Object|null} dupes - Result from detectDuplicates |
| 152 | + * @param {Array|null} nameRefs - Result from findReferences |
| 153 | + * @returns {string} Formatted suggestion message |
| 154 | + * @private |
| 155 | + */ |
| 156 | +function _formatSuggestion(dupes, nameRefs) { |
| 157 | + const parts = []; |
| 158 | + |
| 159 | + if (dupes && dupes.matches && dupes.matches.length > 0) { |
| 160 | + parts.push(`Found ${dupes.matches.length} similar match(es) in codebase`); |
| 161 | + const firstMatch = dupes.matches[0]; |
| 162 | + if (firstMatch.file) { |
| 163 | + parts.push(`Closest: ${firstMatch.file}${firstMatch.line ? ':' + firstMatch.line : ''}`); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + if (nameRefs && nameRefs.length > 0) { |
| 168 | + parts.push(`Symbol already referenced in ${nameRefs.length} location(s)`); |
| 169 | + const firstRef = nameRefs[0]; |
| 170 | + if (firstRef.file) { |
| 171 | + parts.push(`First ref: ${firstRef.file}${firstRef.line ? ':' + firstRef.line : ''}`); |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + parts.push('Consider REUSE or ADAPT before creating new code (IDS Article IV-A)'); |
| 176 | + |
| 177 | + return parts.join('. ') + '.'; |
| 178 | +} |
| 179 | + |
| 180 | +/** |
| 181 | + * Calculate risk level from blast radius count. |
| 182 | + * @param {number} blastRadius - Number of references affected |
| 183 | + * @returns {string} 'LOW' | 'MEDIUM' | 'HIGH' |
| 184 | + * @private |
| 185 | + */ |
| 186 | +function _calculateRiskLevel(blastRadius) { |
| 187 | + if (blastRadius <= RISK_THRESHOLDS.LOW_MAX) { |
| 188 | + return 'LOW'; |
| 189 | + } |
| 190 | + if (blastRadius <= RISK_THRESHOLDS.MEDIUM_MAX) { |
| 191 | + return 'MEDIUM'; |
| 192 | + } |
| 193 | + return 'HIGH'; |
| 194 | +} |
| 195 | + |
| 196 | +module.exports = { |
| 197 | + checkBeforeWriting, |
| 198 | + suggestReuse, |
| 199 | + getConventionsForPath, |
| 200 | + assessRefactoringImpact, |
| 201 | + // Exposed for testing |
| 202 | + _formatSuggestion, |
| 203 | + _calculateRiskLevel, |
| 204 | + RISK_THRESHOLDS, |
| 205 | + REUSE_MIN_REFS, |
| 206 | +}; |
0 commit comments