dependencyiq 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Strategy Generator (GitLab-native)
3
+ * Generates analysis summaries and instructions for GitLab Duo Agentic Chat
4
+ * Uses GitLab's built-in AI models via the Agent Platform
5
+ */
6
+
7
+ /**
8
+ * Generate refactoring strategy analysis for GitLab Chat
9
+ * @param {Object} vulnerability - Vulnerability object
10
+ * @param {Object} codeContext - Code usage information
11
+ * @returns {string} Analysis prompt for GitLab Chat
12
+ */
13
+ function generateRefactoringAnalysis(vulnerability, codeContext = {}) {
14
+ const context = {
15
+ affectedFiles: [],
16
+ usageExamples: [],
17
+ ...codeContext
18
+ };
19
+
20
+ return `
21
+ # Refactoring Analysis: ${vulnerability.package}
22
+
23
+ ## Vulnerability Details
24
+ - **Package**: ${vulnerability.package}
25
+ - **Current**: ${vulnerability.currentVersion}
26
+ - **Target**: ${vulnerability.fixedVersion}
27
+ - **Severity**: ${vulnerability.severity} (CVSS ${vulnerability.cvss})
28
+ - **Issue**: ${vulnerability.vulnerability}
29
+
30
+ ## Code Impact
31
+ - **Files affected**: ${context.affectedFiles?.length || 1}
32
+ ${context.affectedFiles?.map(f => ` - ${f.path || f}`).join('\n') || ' - no Orbit exposure data available'}
33
+
34
+ ## Usage Example
35
+ \`\`\`javascript
36
+ ${context.usageExamples?.[0] || 'const merged = _.merge({}, defaults, userInput);'}
37
+ \`\`\`
38
+
39
+ ## Task for GitLab Duo Chat
40
+
41
+ Generate **3 upgrade strategies** ranked by safety vs speed:
42
+
43
+ ### Strategy 1: Safest Approach
44
+ - What code changes are needed?
45
+ - Why is this the safest?
46
+ - What testing is required?
47
+ - Estimated time to implement?
48
+ - Best for: Large teams, critical services?
49
+
50
+ ### Strategy 2: Recommended Approach (Balanced)
51
+ - What changes would you recommend?
52
+ - Why balance safety and speed?
53
+ - What's the testing strategy?
54
+ - Estimated time?
55
+ - Best for: Most development teams?
56
+
57
+ ### Strategy 3: Fastest Approach
58
+ - What's the minimal viable change?
59
+ - What risks exist with this approach?
60
+ - How to mitigate those risks?
61
+ - Estimated time?
62
+ - Best for: Non-critical services, internal tools?
63
+
64
+ For each strategy, provide:
65
+ - Complete code examples
66
+ - Rationale for the approach
67
+ - Success criteria
68
+ - Potential gotchas
69
+ `;
70
+ }
71
+
72
+ /**
73
+ * Generate migration timeline analysis for GitLab Chat
74
+ * @param {Array} vulnerabilities - Vulnerabilities with risk scores
75
+ * @returns {string} Timeline planning prompt
76
+ */
77
+ function generateMigrationTimeline(vulnerabilities = []) {
78
+ const vulnList = vulnerabilities
79
+ .slice(0, 5)
80
+ .map((v, i) => `${i + 1}. **${v.package}** (Risk ${v.riskScore?.score || '?'}/100, ${v.severity})\n - Issue: ${v.vulnerability}`)
81
+ .join('\n');
82
+
83
+ return `
84
+ # Vulnerability Migration Timeline
85
+
86
+ ## Vulnerabilities to Address (Priority Order)
87
+ ${vulnList}
88
+
89
+ ## Task: Create Phased Migration Plan
90
+
91
+ ### Phase 1: This Week (Most Urgent)
92
+ - Which vulnerabilities should we fix first?
93
+ - Why prioritize these?
94
+ - Estimated team effort?
95
+ - Testing and QA strategy?
96
+ - Deployment approach (direct, canary, feature flag)?
97
+ - What's the rollback procedure?
98
+
99
+ ### Phase 2: Next Week (High Priority)
100
+ - Next batch of vulnerabilities?
101
+ - Expected effort hours?
102
+ - Testing approach?
103
+ - Deployment strategy?
104
+ - Any dependencies between Phase 1 & 2?
105
+
106
+ ### Phase 3: Following Week+ (Medium/Low Priority)
107
+ - Remaining vulnerabilities to fix?
108
+ - Nice-to-have improvements?
109
+ - Documentation updates needed?
110
+ - Estimated total effort?
111
+
112
+ ## For All Phases
113
+
114
+ - Why prioritize in this specific order?
115
+ - What risks if we delay any phase?
116
+ - How do we measure success?
117
+ - Team communication plan?
118
+ - How to track completion?
119
+
120
+ Provide realistic estimates based on:
121
+ - Team size and velocity
122
+ - Complexity of each change
123
+ - Testing requirements
124
+ - Deployment procedures
125
+ `;
126
+ }
127
+
128
+ /**
129
+ * Generate executive summary of vulnerability analysis
130
+ * @param {Array} vulnerabilities - Analyzed vulnerabilities
131
+ * @returns {string} Summary for quick review
132
+ */
133
+ function generateAnalysisSummary(vulnerabilities = []) {
134
+ if (!vulnerabilities || vulnerabilities.length === 0) {
135
+ return '✅ No vulnerabilities found in this project.';
136
+ }
137
+
138
+ const urgent = vulnerabilities.filter(v => v.riskScore?.priority === 'URGENT');
139
+ const high = vulnerabilities.filter(v => v.riskScore?.priority === 'HIGH');
140
+ const medium = vulnerabilities.filter(v => v.riskScore?.priority === 'MEDIUM');
141
+
142
+ let summary = `
143
+ # Vulnerability Analysis Summary
144
+
145
+ ## Overview
146
+ - **Total Found**: ${vulnerabilities.length}
147
+ - 🔴 **URGENT** (Risk 80-100): ${urgent.length}
148
+ - 🟠 **HIGH** (Risk 50-79): ${high.length}
149
+ - 🟡 **MEDIUM** (Risk 20-49): ${medium.length}
150
+
151
+ ## Top 3 Priorities
152
+
153
+ ${vulnerabilities.slice(0, 3).map((v, i) => `
154
+ ### ${i + 1}. ${v.package}
155
+ - **Risk Score**: ${v.riskScore?.score}/100 (${v.riskScore?.priority})
156
+ - **Issue**: ${v.vulnerability}
157
+ - **Severity**: ${v.severity} (CVSS ${v.cvss})
158
+ - **Files Affected**: ${v.affectedFiles?.length || 1}
159
+ ${v.affectedFiles?.slice(0, 2).map(f => ` - ${f.path || f}`).join('\n') || ' - internal usage'}
160
+ `).join('\n')}
161
+
162
+ ## Recommendation
163
+
164
+ ✅ **Focus on URGENT vulnerabilities first** (${urgent.length} found)
165
+ - These have actual code exposure in your project
166
+ - Not just generic CVSS scores—real risk to your systems
167
+
168
+ 🎯 **Expect** to fix all urgents + highs in about 1-2 weeks
169
+ - Phase 1 (this week): URGENT items
170
+ - Phase 2 (next week): HIGH items
171
+ - Phase 3 (ongoing): MEDIUM + LOW
172
+
173
+ 💡 **Next Step**: Ask GitLab Duo Chat for specific upgrade strategies
174
+ - It will analyze your code and suggest safe refactoring approaches
175
+ - It can generate migration plans and timelines
176
+ - Ask it to create a draft MR for Phase 1
177
+ `;
178
+
179
+ return summary;
180
+ }
181
+
182
+ /**
183
+ * Format a vulnerability for human-readable display
184
+ * @param {Object} vuln - Vulnerability object
185
+ * @param {number} index - Position in list
186
+ * @returns {string} Formatted display string
187
+ */
188
+ function formatVulnerabilityCard(vuln, index = 1) {
189
+ const icon = vuln.riskScore?.priority === 'URGENT' ? '🔴'
190
+ : vuln.riskScore?.priority === 'HIGH' ? '🟠'
191
+ : vuln.riskScore?.priority === 'MEDIUM' ? '🟡' : '🟢';
192
+
193
+ return `
194
+ ${icon} **${index}. ${vuln.package} → ${vuln.fixedVersion}**
195
+
196
+ | Field | Value |
197
+ |-------|-------|
198
+ | **Current** | ${vuln.currentVersion} |
199
+ | **Issue** | ${vuln.vulnerability} |
200
+ | **Severity** | ${vuln.severity} (CVSS ${vuln.cvss}) |
201
+ | **Your Risk Score** | ${vuln.riskScore?.score || '?'}/100 (${vuln.riskScore?.priority}) |
202
+ | **Files Affected** | ${vuln.affectedFiles?.length || 1} |
203
+ | **Exposed to API?** | ${vuln.riskScore?.isInPublicAPI ? '✅ Yes' : '❌ No'} |
204
+ | **Effort to Fix** | ${vuln.riskScore?.effortMinutes ? Math.ceil(vuln.riskScore.effortMinutes / 60) + 'h' : '?'} |
205
+ `;
206
+ }
207
+
208
+ /**
209
+ * Generate a merge request description (deterministic template — no AI
210
+ * call). Deeper strategy reasoning is available interactively by asking
211
+ * the DependencyIQ GitLab Duo agent in Chat, which uses GitLab's managed
212
+ * model rather than an external API key.
213
+ * @param {Object} vulnerability - vulnerability being fixed
214
+ * @param {Object} fixResult - result from ecosystemFixers.applyFix
215
+ * @returns {string} PR description markdown
216
+ */
217
+ function generatePRDescription(vulnerability, fixResult = {}) {
218
+ if (fixResult.action === 'remove') {
219
+ return `# Cleanup: Remove unused dependency ${vulnerability.package}
220
+
221
+ ## Summary
222
+ \`${vulnerability.package}\` (${vulnerability.ecosystem}) has a known vulnerability — **${vulnerability.vulnerability}** (${vulnerability.id}) — but **GitLab Orbit confirms zero files in this project import it**. Patching a CVE in code nothing uses doesn't reduce real risk, so this removes the dependency entirely instead of bumping its version.
223
+
224
+ - **Vulnerability that prompted this**: ${vulnerability.id} (${vulnerability.severity}, CVSS ${vulnerability.cvss})
225
+ - **Exposure data**: GitLab Orbit blast-radius query — 0 importers found
226
+ - **Why removal instead of upgrade**: nothing to break by removing it; upgrading would just carry a version of a package you don't use
227
+
228
+ ## Changes
229
+ - Removed \`${vulnerability.package}\` from \`${fixResult.filePath || 'manifest'}\`.
230
+ ${fixResult.followUp ? `- **Follow-up required**: run \`${fixResult.followUp}\` to refresh the lockfile before merging.` : ''}
231
+ ${fixResult.warning ? `- ⚠️ ${fixResult.warning}` : ''}
232
+
233
+ ---
234
+ Generated by DependencyIQ using a GitLab Orbit blast-radius query — not a guess based on CVSS alone.
235
+ `;
236
+ }
237
+
238
+ const sel = vulnerability.versionSelection;
239
+ const floor = vulnerability.osvFloorVersion;
240
+ const isOverride = fixResult.action === 'override';
241
+
242
+ // "Which version?" section — only when a real decision was made (a smart
243
+ // target above the OSV floor, or a major-crossing flag). Shows the
244
+ // reasoning instead of silently taking OSV's minimum.
245
+ let versionSection = '';
246
+ if (sel?.available && (sel.recommendedVersion !== floor || sel.crossesMajor)) {
247
+ versionSection = `
248
+
249
+ ## Version selection
250
+ - **OSV security floor**: ${floor} (minimum version that patches the advisory)
251
+ - **Chosen target**: ${vulnerability.fixedVersion}${sel.settled ? ` — settled (${sel.ageDays}d since release)` : ' — note: recently published'}
252
+ - **Reasoning**: ${sel.rationale}${sel.crossesMajor ? '\n- ⚠️ **Crosses a major version** — review for breaking changes before merging.' : ''}`;
253
+ }
254
+
255
+ const changesSection = isOverride
256
+ ? `## Changes
257
+ - ${vulnerability.package} is a **transitive** dependency (pulled in via ${vulnerability.dependencyChain?.chain ? vulnerability.dependencyChain.chain.join(' → ') : 'another dependency'}), so there's no direct version line to bump.
258
+ - Added an \`overrides\` entry in \`${fixResult.filePath || 'package.json'}\` forcing ${vulnerability.package} to \`>=${floor || vulnerability.fixedVersion}\` (the patched floor) regardless of what the parent declares — the standard fix for the Log4j/Axios class of transitive incident.
259
+ ${fixResult.followUp ? `- **Follow-up required**: run \`${fixResult.followUp}\`.` : ''}
260
+ ${fixResult.warning ? `- ⚠️ ${fixResult.warning}` : ''}`
261
+ : `## Changes
262
+ - Updated \`${fixResult.filePath || 'manifest'}\` to pin ${vulnerability.package} to ${vulnerability.fixedVersion}.
263
+ ${fixResult.followUp ? `- **Follow-up required**: run \`${fixResult.followUp}\` to refresh the lockfile before merging.` : ''}
264
+ ${fixResult.warning ? `- ⚠️ ${fixResult.warning}` : ''}`;
265
+
266
+ return `# Security Update: ${vulnerability.package}
267
+
268
+ ## Summary
269
+ Fixes **${vulnerability.vulnerability}** (${vulnerability.id}) in ${vulnerability.package} (${vulnerability.ecosystem})${isOverride ? ' — a transitive dependency, fixed via an npm override' : ''}
270
+
271
+ - **Current**: ${vulnerability.currentVersion}
272
+ - **Target**: ${isOverride ? `>=${floor || vulnerability.fixedVersion} (transitive override)` : vulnerability.fixedVersion}
273
+ - **Severity**: ${vulnerability.severity} (CVSS ${vulnerability.cvss})
274
+ - **Risk Score**: ${vulnerability.riskScore?.score ?? 'N/A'}/100 (${vulnerability.riskScore?.priority ?? 'unscored'})
275
+ - **Exposure data**: ${vulnerability.riskScore?.exposureDataSource === 'orbit' ? 'GitLab Orbit blast-radius query' : 'unavailable — Orbit not enabled/reachable for this project'}${versionSection}
276
+
277
+ ${changesSection}
278
+
279
+ ## References
280
+ ${(vulnerability.references || []).slice(0, 3).map(r => `- ${r}`).join('\n') || '- ' + vulnerability.cveLink}
281
+
282
+ ---
283
+ Generated by DependencyIQ. Ask the DependencyIQ agent in GitLab Duo Chat for upgrade strategies or a phased migration plan for the remaining findings.
284
+ `;
285
+ }
286
+
287
+ /**
288
+ * Generate a structured, ordered migration plan for one upgrade, using
289
+ * the impact report's real affected-file count and effort estimate
290
+ * rather than a generic template. Deterministic — no AI call.
291
+ * @param {Object} impactReport - result from impactReport.buildImpactReport
292
+ * @returns {Object} { steps: [{order, title, detail}], estimatedHours }
293
+ */
294
+ function generateStructuredMigrationPlan(impactReport) {
295
+ const steps = [];
296
+ let order = 0;
297
+ const nextOrder = () => { order += 1; return order; };
298
+ const { simulation } = impactReport;
299
+
300
+ if (simulation) {
301
+ steps.push({
302
+ order: nextOrder(),
303
+ title: 'Review upgrade impact simulation',
304
+ detail: `${simulation.difficulty} difficulty; ${simulation.relationship} dependency; ${simulation.affectedServicesCount} affected service area(s). Automation mode: ${simulation.automationMode}.`,
305
+ });
306
+ }
307
+
308
+ if (impactReport.affectedFilesCount > 0) {
309
+ steps.push({
310
+ order: nextOrder(),
311
+ title: 'Review affected call sites',
312
+ detail: `${impactReport.affectedFilesCount} file(s) import ${impactReport.package} — read each one to confirm how it's used before changing the version.`,
313
+ });
314
+ }
315
+
316
+ if (impactReport.likelyBreaking) {
317
+ steps.push({
318
+ order: nextOrder(),
319
+ title: 'Check for breaking changes',
320
+ detail: `${impactReport.breakingChangeReasoning} Read the package's changelog/release notes for ${impactReport.from} → ${impactReport.to} before touching code.`,
321
+ });
322
+ steps.push({
323
+ order: nextOrder(),
324
+ title: 'Update call sites for the new major version',
325
+ detail: 'Adjust any usages flagged in the previous step to match the new API surface, guided by the changelog, not a guess.',
326
+ });
327
+ }
328
+
329
+ steps.push({
330
+ order: nextOrder(),
331
+ title: `Bump ${impactReport.package} to ${impactReport.to}`,
332
+ detail: 'Update the manifest (already done automatically if this came from `--fix`) and refresh the lockfile.',
333
+ });
334
+
335
+ steps.push({
336
+ order: nextOrder(),
337
+ title: 'Run the test suite',
338
+ detail: simulation?.safetyChecks?.length
339
+ ? `Confirm nothing regressed before opening or merging the MR. Required checks: ${simulation.safetyChecks.join('; ')}.`
340
+ : 'Confirm nothing regressed before opening or merging the MR.',
341
+ });
342
+
343
+ if (impactReport.affectedFilesCount === 0) {
344
+ steps.unshift({
345
+ order: 0,
346
+ title: 'Confirm this dependency is actually unused',
347
+ detail: 'Orbit found zero importers — consider removing the dependency entirely instead of running this plan (see riskScore.recommendation).',
348
+ });
349
+ }
350
+
351
+ return {
352
+ package: impactReport.package,
353
+ from: impactReport.from,
354
+ to: impactReport.to,
355
+ steps,
356
+ estimatedHours: impactReport.estimatedEffort.hours,
357
+ };
358
+ }
359
+
360
+ /**
361
+ * Render a structured migration plan as markdown.
362
+ */
363
+ function formatMigrationPlan(plan) {
364
+ const stepLines = plan.steps
365
+ .map(s => `${s.order}. **${s.title}**\n ${s.detail}`)
366
+ .join('\n\n');
367
+
368
+ return `## Migration Plan: ${plan.package} ${plan.from} → ${plan.to}
369
+
370
+ ${stepLines}
371
+
372
+ **Estimated effort**: ~${plan.estimatedHours}h
373
+ `;
374
+ }
375
+
376
+ module.exports = {
377
+ generateRefactoringAnalysis,
378
+ generateMigrationTimeline,
379
+ generateAnalysisSummary,
380
+ formatVulnerabilityCard,
381
+ generatePRDescription,
382
+ generateStructuredMigrationPlan,
383
+ formatMigrationPlan
384
+ };
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Upgrade Impact Simulator.
3
+ *
4
+ * Turns the raw impact report into the engineering question leaders ask:
5
+ * "How painful will this remediation be, and what work should happen
6
+ * before the MR is merged?"
7
+ *
8
+ * This is evidence-based, not an API-diff oracle. It uses Orbit-derived
9
+ * affected files, dependency-chain data, exposure categories, and semver
10
+ * distance to identify likely change areas, blockers, checks, and a
11
+ * phased migration plan.
12
+ */
13
+
14
+ const SERVICE_ROOTS = ['services', 'apps', 'packages', 'workspaces'];
15
+
16
+ function filePathOf(file) {
17
+ return String(file?.path || file || '');
18
+ }
19
+
20
+ function inferServiceName(filePath) {
21
+ const normalized = filePath.replace(/\\/g, '/').replace(/^\/+/, '');
22
+ const parts = normalized.split('/').filter(Boolean);
23
+ if (parts.length >= 2 && SERVICE_ROOTS.includes(parts[0])) {
24
+ return `${parts[0]}/${parts[1]}`;
25
+ }
26
+ return 'current repository';
27
+ }
28
+
29
+ function unique(values) {
30
+ return [...new Set(values.filter(Boolean))];
31
+ }
32
+
33
+ function hasPathMatch(paths, patterns) {
34
+ return paths.some(p => patterns.some(pattern => p.toLowerCase().includes(pattern)));
35
+ }
36
+
37
+ function dependencyRelationship(vulnerability) {
38
+ if (vulnerability.recommendation === 'remove') return 'unused';
39
+ const chain = vulnerability.dependencyChain;
40
+ if (!chain?.available) return 'unknown';
41
+ return chain.isDirect ? 'direct' : 'transitive';
42
+ }
43
+
44
+ function difficultyFor({
45
+ affectedFilesCount,
46
+ affectedServicesCount,
47
+ likelyBreaking,
48
+ relationship,
49
+ touchesPublicApi,
50
+ touchesAuth,
51
+ touchesConfig,
52
+ }) {
53
+ if (relationship === 'unused') return 'low';
54
+ if (likelyBreaking && (affectedFilesCount >= 30 || affectedServicesCount >= 5)) return 'critical';
55
+ if (touchesPublicApi && (affectedServicesCount >= 2 || likelyBreaking)) return 'high';
56
+ if (touchesAuth && touchesConfig && affectedServicesCount >= 2) return 'high';
57
+ if (likelyBreaking || affectedFilesCount >= 15 || affectedServicesCount >= 3) return 'high';
58
+ if (affectedFilesCount >= 5 || affectedServicesCount >= 2 || relationship === 'transitive') return 'medium';
59
+ return 'low';
60
+ }
61
+
62
+ function automationMode({ relationship, likelyBreaking, exposureDataSource }) {
63
+ if (relationship === 'unused') return 'remove';
64
+ if (exposureDataSource !== 'orbit') return 'needs-orbit-validation';
65
+ if (relationship === 'transitive') return 'override-and-review';
66
+ if (likelyBreaking) return 'migration-mr-with-review';
67
+ return 'auto-mr-ready';
68
+ }
69
+
70
+ function buildRequiredChanges(vulnerability, impactReport, context) {
71
+ const changes = [];
72
+
73
+ if (context.relationship === 'unused') {
74
+ return [{
75
+ title: 'Remove the dependency instead of upgrading it',
76
+ detail: 'Orbit confirmed zero importing files, so the safest remediation is deleting the dependency and refreshing the lockfile.',
77
+ confidence: 'high',
78
+ }];
79
+ }
80
+
81
+ if (context.relationship === 'transitive') {
82
+ changes.push({
83
+ title: 'Patch the transitive dependency path',
84
+ detail: vulnerability.dependencyChain?.chain
85
+ ? `Resolved chain: ${vulnerability.dependencyChain.chain.join(' -> ')}. Use an ecosystem override or upgrade the parent dependency that pulls it in.`
86
+ : 'The package is transitive, so there is no direct manifest line to bump. Force a secure resolver version or upgrade the parent dependency.',
87
+ confidence: 'high',
88
+ });
89
+ }
90
+
91
+ if (impactReport.affectedFilesCount > 0) {
92
+ changes.push({
93
+ title: 'Review affected import sites',
94
+ detail: `${impactReport.affectedFilesCount} file(s) import this package across ${context.affectedServicesCount} service area(s). These are the files most likely to need code changes.`,
95
+ confidence: impactReport.exposureDataSource === 'orbit' ? 'high' : 'low',
96
+ });
97
+ }
98
+
99
+ if (impactReport.likelyBreaking) {
100
+ changes.push({
101
+ title: 'Check release notes for removed or renamed APIs',
102
+ detail: impactReport.breakingChangeReasoning,
103
+ confidence: 'medium',
104
+ });
105
+ }
106
+
107
+ if (context.touchesPublicApi) {
108
+ changes.push({
109
+ title: 'Validate public API compatibility',
110
+ detail: 'At least one importing file is classified as public API, so contract tests or consumer checks should run before merge.',
111
+ confidence: 'high',
112
+ });
113
+ }
114
+
115
+ if (context.touchesConfig) {
116
+ changes.push({
117
+ title: 'Review configuration and schema usage',
118
+ detail: 'Affected paths include config/schema/env-style files. Verify default values, renamed options, and deployment configuration.',
119
+ confidence: 'medium',
120
+ });
121
+ }
122
+
123
+ if (context.touchesAuth) {
124
+ changes.push({
125
+ title: 'Review authentication or authorization adapters',
126
+ detail: 'Affected paths include auth/security/session-style code. Prioritize regression tests around login, token handling, and permissions.',
127
+ confidence: 'medium',
128
+ });
129
+ }
130
+
131
+ if (changes.length === 0) {
132
+ changes.push({
133
+ title: 'Apply the version bump and run verification',
134
+ detail: 'No affected import sites were available, so treat this as a manifest-only remediation until Orbit data is present.',
135
+ confidence: 'low',
136
+ });
137
+ }
138
+
139
+ return changes;
140
+ }
141
+
142
+ function buildSafetyChecks(context) {
143
+ const checks = ['Run the project test suite after the manifest and lockfile update'];
144
+ if (context.touchesPublicApi) checks.push('Run API/contract tests for public entry points');
145
+ if (context.touchesAuth) checks.push('Run auth and permission regression tests');
146
+ if (context.touchesConfig) checks.push('Validate deployment config and environment defaults');
147
+ if (context.relationship === 'transitive') checks.push('Run dependency-tree verification to confirm the patched version resolves');
148
+ return checks;
149
+ }
150
+
151
+ function buildMigrationPhases(context) {
152
+ if (context.relationship === 'unused') {
153
+ return [
154
+ { phase: 1, title: 'Remove', detail: 'Delete the unused dependency from the manifest.' },
155
+ { phase: 2, title: 'Verify', detail: 'Refresh the lockfile and run tests to prove removal is safe.' },
156
+ ];
157
+ }
158
+
159
+ const phases = [
160
+ { phase: 1, title: 'Map exposure', detail: 'Use Orbit evidence to review each affected file and service area.' },
161
+ ];
162
+
163
+ if (context.likelyBreaking) {
164
+ phases.push({ phase: 2, title: 'Adapt code', detail: 'Apply any source changes required by the major-version migration notes.' });
165
+ }
166
+
167
+ phases.push(
168
+ { phase: phases.length + 1, title: 'Upgrade', detail: 'Apply the dependency fix and refresh the lockfile with the ecosystem package manager.' },
169
+ { phase: phases.length + 2, title: 'Verify and ship', detail: 'Run safety checks, open the MR, and let a human review before merge.' }
170
+ );
171
+
172
+ return phases;
173
+ }
174
+
175
+ function buildUpgradeImpactSimulation(vulnerability, impactReport) {
176
+ const paths = (vulnerability.affectedFiles || []).map(filePathOf);
177
+ const affectedServices = unique(paths.map(inferServiceName));
178
+ const relationship = dependencyRelationship(vulnerability);
179
+ const context = {
180
+ relationship,
181
+ affectedServices,
182
+ affectedServicesCount: affectedServices.length,
183
+ affectedFilesCount: impactReport.affectedFilesCount,
184
+ likelyBreaking: impactReport.likelyBreaking,
185
+ exposureDataSource: impactReport.exposureDataSource,
186
+ touchesPublicApi: (vulnerability.affectedFiles || []).some(f => f.category === 'public-api'),
187
+ touchesConfig: hasPathMatch(paths, ['config', 'schema', '.env', '.yaml', '.yml', '.json']),
188
+ touchesAuth: hasPathMatch(paths, ['auth', 'oauth', 'jwt', 'session', 'permission']),
189
+ };
190
+
191
+ return {
192
+ package: vulnerability.package,
193
+ from: vulnerability.currentVersion,
194
+ to: vulnerability.fixedVersion,
195
+ relationship,
196
+ difficulty: difficultyFor(context),
197
+ automationMode: automationMode(context),
198
+ affectedServicesCount: context.affectedServicesCount,
199
+ affectedServices,
200
+ affectedFilesCount: context.affectedFilesCount,
201
+ requiredChanges: buildRequiredChanges(vulnerability, impactReport, context),
202
+ safetyChecks: buildSafetyChecks(context),
203
+ migrationPhases: buildMigrationPhases(context),
204
+ };
205
+ }
206
+
207
+ function formatUpgradeImpactSimulation(simulation) {
208
+ const services = simulation.affectedServices.length
209
+ ? simulation.affectedServices.join(', ')
210
+ : 'none confirmed';
211
+ const changes = simulation.requiredChanges
212
+ .map(change => ` - ${change.title} (${change.confidence} confidence): ${change.detail}`)
213
+ .join('\n');
214
+ const checks = simulation.safetyChecks.map(check => ` - ${check}`).join('\n');
215
+ const phases = simulation.migrationPhases
216
+ .map(phase => ` ${phase.phase}. ${phase.title}: ${phase.detail}`)
217
+ .join('\n');
218
+
219
+ return `## Upgrade Impact Simulator
220
+
221
+ - **Dependency relationship**: ${simulation.relationship}
222
+ - **Upgrade difficulty**: ${simulation.difficulty}
223
+ - **Automation mode**: ${simulation.automationMode}
224
+ - **Affected services**: ${simulation.affectedServicesCount} (${services})
225
+ - **Affected files**: ${simulation.affectedFilesCount}
226
+
227
+ ### Required changes
228
+ ${changes}
229
+
230
+ ### Safety checks
231
+ ${checks}
232
+
233
+ ### Migration phases
234
+ ${phases}
235
+ `;
236
+ }
237
+
238
+ module.exports = {
239
+ buildUpgradeImpactSimulation,
240
+ formatUpgradeImpactSimulation,
241
+ };